Bolsillo permite separar dinero por propósito, registrar ingresos y gastos y ver el saldo disponible en tiempo real. La interfaz es mobile-first y el backend usa Convex con identidad de Clerk.
- Node.js 22 o superior
- Una aplicación de Clerk
- Un proyecto de Convex
npm install
cp .env.example .env.localCompletá .env.local con:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYyCLERK_SECRET_KEY, desde Clerk.CONVEX_DEPLOYMENTyNEXT_PUBLIC_CONVEX_URL, al ejecutarnpm run convex:dev.CLERK_JWT_ISSUER_DOMAIN, el dominio issuer de la instancia de Clerk, sin una barra final.
- En Clerk, creá un JWT template llamado exactamente
convexusando la plantilla de Convex. - Configurá
CLERK_JWT_ISSUER_DOMAINen el deployment de Convex:
npx convex env set CLERK_JWT_ISSUER_DOMAIN https://tu-instancia.clerk.accounts.dev- Vinculá el proyecto y generá los tipos:
npm run convex:devConvex toma el propietario exclusivamente de ctx.auth.getUserIdentity().subject; ningún userId enviado por el navegador se acepta como fuente de autorización.
La función transactions.files permite adjuntar hasta cinco archivos JPG, PNG, WebP, PDF o TXT de 2 MB cada uno. Está deshabilitada por defecto y solo un superadmin puede habilitarla por cuenta.
Antes del merge, seguí la guía de configuración de producción: bucket privado, CORS, variables de Convex y Vercel, y activación por cuenta después del deploy.
Usá buckets separados para desarrollo y producción. Los buckets deben permanecer privados:
npx wrangler login
npx wrangler r2 bucket create bolsillo-files-dev
npx wrangler r2 bucket create bolsillo-files-prodEn Cloudflare, creá para cada bucket un token R2 con permiso Object Read & Write limitado exclusivamente a ese bucket. Guardá el Access Key ID y el Secret Access Key cuando se muestren; el secreto no vuelve a estar disponible.
Copiá r2/cors.example.json, reemplazá https://bolsillo.example.com por el origen real de la aplicación y aplicá la política al bucket correspondiente:
cp r2/cors.example.json r2/cors.json
npx wrangler r2 bucket cors set bolsillo-files-dev --file r2/cors.json
npx wrangler r2 bucket cors list bolsillo-files-devr2/cors.json es configuración local y no debe contener credenciales. Agregá explícitamente cada origen de Preview que necesite probar cargas; R2 valida el origen aunque la URL ya esté firmada.
Las credenciales se usan únicamente desde Node actions de Convex. Configuralas en cada deployment, sin el prefijo NEXT_PUBLIC_:
npx convex env set R2_ACCOUNT_ID
npx convex env set R2_BUCKET_NAME bolsillo-files-dev
npx convex env set R2_ACCESS_KEY_ID
npx convex env set R2_SECRET_ACCESS_KEY
npx convex env set R2_PRESIGN_TTL_SECONDS 300Repetí los comandos con --prod y el bucket de producción. Omitir el valor de los secretos permite ingresarlos interactivamente sin guardarlos en el historial del shell.
Las cargas usan URLs PUT firmadas por cinco minutos. Antes de guardar el movimiento, Convex vuelve a descargar cada objeto desde R2 y valida tamaño, MIME y firma del contenido. Las vistas previas y descargas solicitan URLs GET nuevas; las URLs firmadas nunca se persisten en la base de datos.
En dos terminales:
npm run convex:dev
npm run devLa app estará disponible en http://localhost:3000.
Podés probar sin cuenta ni credenciales de Cloudflare usando MinIO + Convex local. MinIO implementa la API S3 que usa el feature, incluidas las URLs firmadas. Es una prueba de compatibilidad S3; la integración final con R2 se verifica contra un bucket real de desarrollo. Clerk sigue usando tu instancia configurada y requiere conexión a Internet.
El instalador incluido funciona en Linux x64, sin Docker. Descarga una versión fija de MinIO Community (AGPLv3) y verifica su SHA-256 antes de ejecutarla. Convex usa Node.js 22 para sus acciones.
Primera vez, con Clerk y CLERK_JWT_ISSUER_DOMAIN completos en .env.local:
npx convex deployment create local --selectSi ya existe un deployment local, usá npx convex deployment select local.
La selección actualiza las URLs de Convex en .env.local. La base local empieza
vacía y conserva sus propios datos, separados del deployment cloud.
Más información: deployments locales de Convex.
Detené los servidores de desarrollo anteriores y arrancá todo con:
npm run dev:localEl comando inicia MinIO, crea el bucket privado bolsillo-files-local si no existe,
configura CORS y las variables del backend local, espera a que Convex compile e
inicia Next.js. Rechaza deployments cloud y puertos ocupados. Ctrl+C detiene los
tres servicios; los datos persisten entre arranques.
| Servicio | Dirección / almacenamiento |
|---|---|
| App | http://localhost:3000 |
| Convex local | http://127.0.0.1:3210 (puerto asignado por Convex) |
| API S3 local | http://127.0.0.1:9000 |
| Consola MinIO | http://127.0.0.1:9001 |
| Archivos | .local/minio/ |
| Base de datos | .convex/ |
La consola MinIO usa bolsillo-local / bolsillo-local-testing-only: son
credenciales fijas solo de testing local, y MinIO escucha únicamente en
loopback. CORS permite http://localhost:3000 y http://127.0.0.1:3000.
.local/ y .convex/ están excluidos de Git.
Con los servicios corriendo, habilitá los adjuntos para tu correo registrado en Clerk:
npm run local:enable-files -- tu-correo@example.comEste comando consulta el usuario en Clerk, crea/actualiza su cuenta en Convex
local, le da rol superadmin local y habilita transactions.files usando la
mutación administrativa existente. No cambia roles en Clerk ni en Convex cloud.
También podés habilitar otras cuentas desde /superadmin.
En la app, creá un bolsillo y un movimiento, adjuntá un TXT, PDF o imagen de hasta 2 MB, guardalo y probá vista previa, descarga, renombre y eliminación. Hay una prueba automatizada contra los servicios reales locales:
npm run test:local-filesVerifica límites, CORS, carga, validación en Convex, descarga, firmas, acceso privado, protección contra sobrescritura, renombre y borrado físico. Crea una cuenta técnica local y elimina el bolsillo y movimiento de prueba al terminar.
Con npm run dev:local corriendo, ejecutá:
npx playwright install chromium
npm run test:files:reportEl recorrido usa una cuenta dedicada bolsillo.qa+clerk_test@example.com,
comprobantes ficticios y Chromium. Crea doce videos WebM, capturas, descargas
verificadas y un informe HTML en output/transaction-files-qa/<fecha>/.
output/transaction-files-qa/index.html abre el último informe. Los datos de
demostración permanecen en la cuenta QA para poder revisarlos; los escenarios de
borrado eliminan sus propios movimientos y archivos.
Se verifican los cinco formatos, persistencia, vistas previas, descargas, edición, arrastre, cancelación, límites, contenido inválido, reintentos, permisos, móvil, eliminación de movimientos, eliminación de bolsillos y edición concurrente desde dos pestañas. El escenario de recuperación comprueba archivado y revocación de permisos con el editor abierto, reintento de lotes vencidos o inexistentes, confirmación idempotente tras perder una respuesta y cancelación sin esperar a Convex. Usa fallos controlados y mantiene la app accesible; no verifica navegación sin conexión. También se comprueban firmas, expiración y aislamiento entre cuentas contra el almacenamiento local.
El comando inicia y detiene un relay HTTP exclusivo para las solicitudes reales a Clerk, limitado a loopback y al dominio de desarrollo configurado. Resuelve el dominio antes de abrir Chromium para evitar errores intermitentes de DNS en el entorno de QA. La app, Convex y MinIO reciben las solicitudes de los escenarios directamente; las respuestas de autenticación no se simulan. Las sesiones y credenciales no se guardan en el informe.
Si el relay no logra conectarse por IPv6, usá
QA_RELAY_FAMILY=4 npm run test:files:report para probar con IPv4.
Podés abrir el HTML directamente o servir la carpeta:
python3 -m http.server 4173 --bind 127.0.0.1 --directory output/transaction-files-qaEl informe quedará en http://localhost:4173.
Para volver a Convex cloud, detené dev:local y ejecutá:
npx convex deployment select dev
npm run convex:dev
# En otra terminal:
npm run devPara probar con R2 real desde localhost, configurá las variables R2_* en el
deployment cloud como se describe arriba y aplicá r2/cors.local.json al bucket
dedicado con npx wrangler r2 bucket cors set bolsillo-files-dev --file r2/cors.local.json.
Ese comando reemplaza la política CORS: conservá los otros orígenes si compartís
el bucket. Escribir variables solamente en .env.local no las configura en Convex.
R2_LOCAL_ENDPOINT es exclusivo del emulador local y no debe configurarse en cloud.
Para abrir http://wbox.local:3000, iniciá Next con
npm run dev -- --hostname 0.0.0.0 --port 3000. El hostname está incluido en
allowedDevOrigins. En .env.local, las URLs NEXT_PUBLIC_CONVEX_URL y
NEXT_PUBLIC_CONVEX_SITE_URL deben usar wbox.local y los puertos del backend,
porque el navegador del otro equipo no puede acceder al localhost del servidor.
Convex también debe escuchar en la interfaz de red. Al ejecutar convex dev,
revisá esas URLs: el CLI puede volver a escribirlas con 127.0.0.1.
Si el entorno usa CONVEX_SELF_HOSTED_URL, Next en desarrollo redirige
/__convex/* a ese backend y el navegador usa automáticamente el mismo origen
de la app para conectarse, incluidos los WebSockets. En este caso Convex puede
permanecer en loopback y no hace falta abrir su puerto en el firewall. Convex
valida los tokens y permisos de cada solicitud. Esta redirección está desactivada
fuera de desarrollo y los entornos cloud conservan su URL habitual.
Si usás MinIO, habilitá su API en la interfaz de red y agregá
http://wbox.local:3000 a sus orígenes CORS. Conservá R2_LOCAL_ENDPOINT con
loopback para las operaciones del servidor y configurá en Convex
R2_LOCAL_PUBLIC_ENDPOINT=http://wbox.local:<puerto-minio> para las URLs firmadas
que usa el navegador. El bucket sigue siendo privado. Esta configuración de red
es adicional al entorno dev:local, que por defecto escucha solo en loopback.
Para usar únicamente el puerto de la app también para los adjuntos, configurá
LOCAL_STORAGE_PROXY_TARGET=http://127.0.0.1:<puerto-minio> en .env.local y
R2_LOCAL_PROXY_URL=http://wbox.local:3000/__files en Convex. En este modo MinIO
puede permanecer en loopback. El proxy de desarrollo conserva la ruta y firma S3
originales al reenviar: las cargas y descargas siguen requiriendo un enlace
firmado vigente. R2_LOCAL_PROXY_URL tiene prioridad sobre
R2_LOCAL_PUBLIC_ENDPOINT y solo se aplica al emulador local. Las cargas tienen
un límite de espera de 45 segundos y muestran cómo reintentar si la conexión falla.
npm run typecheck
npm run lint
npm test
npm run build- Importá el repositorio en Vercel.
- Agregá las variables de Clerk y
NEXT_PUBLIC_CONVEX_URLal proyecto de Vercel. - Generá una Production Deploy Key de Convex con permiso
deployment:deployy guardala en Vercel comoCONVEX_DEPLOY_KEY, limitada al ambiente Production. - En Convex, configurá
CLERK_JWT_ISSUER_DOMAINpara producción. - Desplegá el frontend en Vercel. El
buildCommanddevercel.jsonpublica primero las funciones, el schema y los índices de Convex; si ese paso falla, la release también falla.
Los Preview Deployments ejecutan únicamente el build de Next.js. Para darles un backend Convex aislado, configurá adicionalmente una Preview Deploy Key siguiendo la guía oficial de Convex.
Las rutas /sign-in y /sign-up son públicas. El resto queda protegido por Clerk en proxy.ts y cada query o mutation vuelve a comprobar identidad y propiedad en Convex.
Bolsillo mantiene en Convex un registro local de usuarios y cuentas. Cada usuario de Clerk recibe una cuenta personal; los bolsillos nuevos se vinculan a esa cuenta y los registros históricos se migran de forma idempotente.
Para crear el primer superadmin en un deployment:
npx convex env set SUPERADMIN_CLERK_USER_IDS user_xxxDespués, iniciá sesión una vez. users.ensureCurrent persistirá el rol superadmin en Convex. Confirmá el acceso a /superadmin y eliminá la variable de bootstrap:
npx convex env remove SUPERADMIN_CLERK_USER_IDSDesde /superadmin se pueden suspender o reactivar cuentas, administrar roles de plataforma, configurar acceso por función, aplicar límites de bolsillos y consultar la auditoría. Las restricciones se validan nuevamente en las funciones de Convex; la interfaz solo refleja el resultado.
La sesión crea o actualiza la cuenta actual de forma síncrona. Para mantener nombre, correo, avatar y eliminaciones actualizados aunque el usuario no vuelva a iniciar sesión, configurá también un webhook de Clerk:
- En Clerk, creá un endpoint
https://<deployment>.convex.site/clerk-users-webhook. - Suscribilo a
user.created,user.updatedyuser.deleted. - Guardá su signing secret en el deployment de Convex:
npx convex env set CLERK_WEBHOOK_SIGNING_SECRET whsec_xxxLa firma se verifica antes de procesar cada evento. Las eliminaciones de Clerk conservan los datos financieros y suspenden la cuenta correspondiente.
wallets.create: creación y restauración de bolsillos; admite un límite de bolsillos activos.transactions.manage: creación, edición y eliminación de movimientos.tags.manage: creación, edición y eliminación de tags.wallets.share: generación y uso del resumen compartible.transactions.files: carga, vista previa, descarga y eliminación de archivos privados en movimientos; deshabilitada por defecto.
El flujo es Empezar → Completar → Confirmar. Empezar muestra Completar manualmente antes de la opción de comprobante. La lectura con IA se solicita explícitamente en ese primer paso, eligiendo fotos o archivos para un solo movimiento. Completar reúne los campos y los adjuntos opcionales: tanto la entrada manual como la asistida pueden agregar archivos sin ejecutar IA. Confirmar muestra el impacto en el saldo antes de registrar y lleva al detalle de consulta, desde donde se puede editar o eliminar con confirmación.
Los movimientos nuevos siempre empiezan en Empezar; continuar un borrador de creación abre Completar con los datos y archivos guardados. El bolsillo separa Registrados y Pendientes; los filtros de movimientos mantienen visible el saldo global.
Editar un movimiento registrado sigue Completar → Confirmar, sin ofrecer IA ni Continuar después y sin crear borradores. Campos, nombres, eliminaciones y archivos nuevos quedan en memoria hasta Guardar cambios. Abrir, modificar, adjuntar y cancelar antes de guardar no escriben en el servidor; cancelar conserva el movimiento original y su saldo. Los borradores de edición antiguos siguen recuperándose mediante su enlace explícito, pero el servidor impide crear otros.
- Desplegá las funciones, el esquema y el índice de pendientes de Convex antes de servir el nuevo frontend. Los campos nuevos son opcionales y conservan los registros existentes; los lotes de carga incorporan
expectedRevisionycurrencypara proteger la edición.convex.jsonincluye las dependencias nativas necesarias para preparar imágenes y PDF en las acciones Node 22. - Configurá
ANTHROPIC_API_KEYen el entorno de Convex (Dashboard → Settings → Environment Variables). Una variable en el frontend o solamente en.env.localno configura las acciones de Convex. Nunca uses variablesNEXT_PUBLICpara la clave. - En Superadmin → Cuenta → Acceso a funciones, habilitá Archivos en movimientos y Leer comprobantes con IA. La IA está apagada por defecto y usa el mismo alcance por cuenta que el flag de archivos.
- El límite inicial es 30 análisis por mes y cuenta, configurable en ese panel. El cupo se renueva el día 1 a las 00:00 UTC. Desactivar IA no impide completar manualmente un borrador con permiso de administrar movimientos. Agregar, renombrar o quitar archivos requiere el permiso de archivos; editar solo los campos conserva los adjuntos existentes aunque ese permiso se desactive.
Se utiliza el SDK oficial @anthropic-ai/sdk, Messages API y el modelo exacto claude-haiku-5-5, mediante el endpoint oficial de Anthropic. No se requiere configurar una URL del proveedor. La guía de Claude Haiku 5.5 describe el modelo y el razonamiento adaptativo; usamos esfuerzo low y un límite de 8192 tokens para dejar espacio al razonamiento y al resultado. Pedimos salida estructurada con JSON Schema derivada del esquema Zod y conservamos la validación por campo en el servidor. Las respuestas rechazadas o truncadas no se aplican, pero conservan sus métricas de consumo. No hay herramientas del agente, navegación ni escritura automática del movimiento.
Las lecturas se ejecutan en una acción programada y persistente. El objetivo de experiencia es 5–10 segundos, sin garantizar esa latencia: la solicitud tiene un timeout de 30 segundos y un control de cierre a los 60 segundos. No hay reintentos automáticos del SDK. Un clic repetido recupera la extracción activa o el resultado ya obtenido para esos archivos, sin otra llamada. «Volver a leer» solicita explícitamente un nuevo análisis y consume otro cupo. Cambiar la selección de archivos para lectura invalida el resultado y sus revisiones; agregar respaldos en Completar conserva esa selección y el resultado. Elegir Completar manualmente cancela una lectura pendiente e impide aplicar respuestas tardías.
- Hasta 5 archivos de 2 MB almacenados cada uno: JPG, PNG, WebP, PDF y TXT. Las fotos de hasta 20 MB se pueden reducir en el navegador, conservando una resolución legible; las imágenes decodificadas se limitan a 40 megapíxeles. HEIC no forma parte de esta versión: se explica cómo elegir JPG.
- La lectura con IA conserva los bytes y la resolución de las imágenes ya subidas; solo se recodifican cuando hay que corregir una orientación EXIF. No se aplica una segunda compresión JPEG a capturas PNG ni a fotos que ya cumplen el límite.
- Los PDF se renderizan en el servidor, con un máximo de 10 páginas en total por análisis. No se omiten páginas en silencio. Los PDF protegidos, dañados o demasiado extensos devuelven un error. TXT se limita a 40.000 caracteres para la lectura. Los originales PDF/TXT permanecen intactos en R2.
- Los objetos R2 son privados, con URLs firmadas de corta duración. El servidor valida tamaño, tipo, contenido, cuenta, bolsillo y relación con el borrador antes de leerlos. Al confirmar se guardan todos los adjuntos y se conserva cuáles fueron fuente de la lectura, separados de los respaldos agregados sin IA. El comprobante fuente cuenta dentro del límite de cinco archivos.
- Los borradores de creación manuales y asistidos conservan archivos, datos, resultado y decisiones de revisión durante 24 horas desde su creación. Los cambios se guardan tras 650 ms de inactividad; Continuar después espera el guardado. Se retoman en Completar desde Pendientes o la URL
?draft=…. Los borradores no cambian el saldo. Guardar es idempotente. Las cargas fallidas permiten reintentar o continuar sin esos archivos; no se ofrece funcionamiento sin conexión. - La edición normal no tiene autoguardado. Guardar cambios verifica la revisión del movimiento y la moneda; cambiar archivos también verifica su revisión. Si hay cargas, el servidor repite estas comprobaciones al finalizar. Un conflicto conserva el trabajo local y evita sobrescribir otra edición. Quitar un comprobante también elimina su referencia como fuente de lectura. Si el bolsillo se archiva o se revoca el permiso de administrar movimientos con el editor abierto, un aviso bloquea el guardado y conserva los valores y archivos hasta restaurar el acceso o cancelar.
- En una edición, un lote vencido o inexistente se descarta del intento para volver a cargar los mismos archivos al reintentar; una respuesta incierta conserva el lote para finalizar sin duplicados. Cancelar tras un intento de carga inicia la limpieza pendiente sin esperar su respuesta. La navegación necesita que la app siga accesible. Los enlaces internos y el cierre o recarga protegen cambios locales, pero el historial del navegador puede abandonarlos; no se promete persistencia local al salir.
- Confianza por campo:
high,medium,low,unknown, con motivo y evidencia de archivo/página. Los valores altos/medios completan campos vacíos que no fueron revisados. Los valores bajos requieren Usar este dato; los desconocidos no se completan automáticamente. Las sugerencias que difieren del dato actual se muestran junto al campo, con su explicación y acciones para usar o conservar el dato. Después de una lectura útil, el monto siempre requiere Confirmar monto revisado antes de avanzar, incluso si tiene confianza alta. Editarlo vuelve a requerir esa confirmación. La confianza es una estimación del modelo, no un porcentaje de exactitud. - Se sugieren únicamente tipo, monto, descripción, fecha, notas y tags existentes. Si la moneda del comprobante es distinta, el monto sugerido no se aplica: hay que ingresar y revisar el monto en la moneda del bolsillo. Si cambia la moneda del bolsillo, se bloquea el guardado: al crear, revisá el monto en un nuevo borrador; al editar, volvé a abrir el movimiento y revisalo en la moneda actual. No hay conversión automática ni creación automática de tags. Los posibles duplicados generan advertencias; la coincidencia usa bolsillo, fecha, monto y tipo cuando está disponible.
- CRC y USD admiten hasta dos decimales: los totales impresos como
45181.00y45181.50conservan su valor exacto. Todos los movimientos usan centésimos enteros; los registros CRC históricos se convierten una sola vez mediante una migración respaldada y verificada. Ver precisión y migración de montos. La respuesta de IA se valida por campo: un campo mal formado queda pendiente de revisión y no elimina los otros datos válidos. Una respuesta sin datos utilizables sigue siendo un error.
Al descartar o vencer un borrador se eliminan sus cargas pendientes mediante la cola de limpieza de R2. Los archivos ya guardados en un movimiento se conservan. El resultado con contenido del documento se elimina al vencer el borrador; los registros operativos se conservan durante 30 días y los totales mensuales permanecen disponibles. El cron horario reconcilia eliminaciones pendientes. Borrar el movimiento o bolsillo también limpia sus borradores asociados.
La guía de producción de lectura de comprobantes describe las variables, el despliegue y la activación por cuenta.
Superadmin muestra solicitudes enviadas, reservas, errores, tokens de entrada/salida y duraciones. Un envío al proveedor consume cupo aunque falle, se cancele o el documento resulte irrelevante; los errores previos al envío liberan la reserva. Si una acción se interrumpe después de reservar el envío, el cupo se conserva de forma prudente. Una respuesta sin métricas no permite conocer su costo real.
Para estimar costos, configurá también ANTHROPIC_INPUT_USD_PER_MILLION y ANTHROPIC_OUTPUT_USD_PER_MILLION según las tarifas de Anthropic. La estimación es tokens × tarifa configurada; no contempla descuentos de caché ni sustituye la factura del proveedor. Sin tarifas aparece “—”. No se guardan claves, URLs firmadas, prompts ni mensajes crudos del proveedor en los registros administrativos.
Ejecutá npm run lint, npm run typecheck, npm test y npm run build. Los tests cubren aislamiento entre cuentas, archivos verificados, concurrencia, expiración, límites/idempotencia, cancelación, monedas/duplicados, validación de confianza, renderizado PDF y el formato real de la solicitud del SDK con un transporte simulado. La precisión y latencia de Claude Haiku 5.5 requieren además una prueba con la clave definitiva, usando comprobantes de prueba representativos.
La dirección implementada, la arquitectura y la evidencia de validación están en Flujo de movimientos. La revisión previa a integrar main registra 111 tests en 18 archivos, lint, typecheck y build aprobados, más 12 escenarios de adjuntos verificados en móvil y escritorio. El build requirió un reintento por descarga de fuentes. Las pruebas reales usaron localhost:3034 con servicios locales aislados; la suite estándar main-flow no se ejecutó contra el servidor compartido localhost:3000 ni se llamó al proveedor externo de IA. La revisión también verificó las correcciones de recuperación de edición.
La validación de integración con main pasó 177 tests en 21 archivos, lint, typecheck y build. El navegador verificó montos CRC con centésimos a través de creación, adjuntos, recuperación de borrador y edición sin crear borradores, además de navegación y totales de estadísticas. La integración se verificó en móvil y escritorio.
El plan de IA y la comparación de prototipos se conservan como antecedentes. Los resultados de revisión previos a integrar origin/main y la validación de integración se documentan por separado en flujo de movimientos. No se desplegó a producción. Los clientes antiguos que sigan abiertos pueden necesitar recargarse para mostrar la nueva confirmación explícita del monto y dejar de intentar crear borradores de edición.