01-overview.md
Visión general · Task Cloud
Task Cloud es una Progressive Web App de gestión de tareas personales con soporte offline real y sincronización en la nube.
El repositorio es un monorepo npm workspaces + Turborepo:
apps/web— Angular 22 + Ionic 8 (PWA)apps/api— NestJS 11 sobre Neon Postgres
En producción se despliega como un único proyecto Vercel: el front se sirve como estáticos y Nest se envuelve en una función serverless (api/index.js) detrás de /api/* (same-origin, sin CORS).
Enlaces
- Web: https://task-manager-cristiancode.vercel.app
- GitHub: Criscode2022/task-manager-cloud
- Health:
GET /api/health→{ "ok": true, "service": "task-cloud-nest-api" }
Stack
| Capa | Tecnología |
|---|---|
| Monorepo | Turborepo + npm workspaces |
| Web | Angular 22, Ionic 8, Tailwind, service worker |
| API | NestJS 11, jose, bcryptjs |
| Datos | Neon Postgres (@neondatabase/serverless) |
| Deploy | Vercel (static + serverless function) |
02-product.md
Producto y features
Problema
Las apps de tareas suelen exigir registro completo, fallan sin red o acoplan el cliente a un BaaS monolítico. El objetivo era una PWA instalable, usable 100% offline, con sync opcional controlada por una API propia sobre Postgres serverless (Nest + Neon + Vercel).
Solución
Cliente offline-first con Ionic Storage. Con sesión activa, sube y baja tareas vía Nest. Auth ligera por PIN de 8 dígitos (hash + pepper) y sesiones JWT. Deploy unificado en Vercel.
Características
CRUD de tareas
Crear, editar, completar y eliminar con feedback inmediato en la UI Ionic.
Prioridad y tags
Campos priority (low / medium / high) y tags[] en Postgres, con índices GIN.
Filtros por estado
Vistas de pendientes, completadas o todas, orientadas al uso diario en móvil.
Offline-first
Ionic Storage mantiene la lista usable sin red; la nube no es SPOF de la UI.
Sync unitario y bulk
API REST: POST /tasks, POST /tasks/bulk, PUT, DELETE protegidos por JWT.
Auth PIN + sesión
Registro/login con PIN, JWT (sub + sid), tabla sessions y rate-limit en auth.
PWA instalable
Service worker Angular y layout app-like en móvil y escritorio.
i18n
ngx-translate con idiomas EN/ES en el cliente.
03-architecture.md
Arquitectura
Diagrama lógico
Browser (PWA Ionic/Angular)
│ offline: Ionic Storage
│ online + sesión: HTTP /api/*
▼
NestJS (apps/api) ──@neondatabase/serverless──► Neon Postgres
▲
api/index.js (Vercel serverless, warm cache)
mcp-server/ (opcional, Python FastMCP) ──► Neon
Capas
- Cliente — Angular 22 + Ionic 8 + Tailwind. Servicios de dominio:
TaskService,TaskNeonService,NeonApiService. Techo v22×Ionic:11-angular-skill.md. - API — NestJS 11: módulos
auth,users,tasks,health, rate-limit y exception filter. - Datos — Neon Postgres:
users,tasks,sessions. - Deploy — Un proyecto Vercel: turbo build web+api; output
apps/web/www; rewrites/api/*→ función. - Local — API
:3001, web:4200con proxy/api;npm run start:full.
Principios
- Offline primero; sync opcional.
- Secretos solo en servidor (
DATABASE_URL,JWT_SECRET,PIN_PEPPER). - Same-origin en producción (
API_BASE_URL=/api).
04-monorepo.md
Layout del monorepo
| Path | Rol |
|---|---|
apps/web | PWA Ionic/Angular (output www/) |
apps/api | NestJS (dist/) consumido por la función Vercel |
api/index.js | Entry serverless: boot Nest + cache warm |
packages/ | Reservado para shared packages |
mcp-server/ | MCP Python opcional (FastMCP + uv) |
vercel.json | Build, functions, rewrites SPA + /api |
turbo.json | Tasks, env y remote cache |
Scripts raíz
| Script | Descripción |
|---|---|
npm run api | Nest en watch |
npm start | Angular + proxy /api |
npm run start:full | API + web vía turbo |
npm run build | Build de todos los packages |
npm run build:web / build:api | Build selectivo |
npm run test:ci | Tests web headless |
npm run test:api | Smoke HTTP de la API |
Workspaces
"workspaces": ["apps/*", "packages/*"]
Package manager: npm 10. Node ≥ 20.
05-api.md
Superficie API (Nest)
Prefijo global: /api. Rutas de tareas y sesión protegidas con Authorization: Bearer <jwt>.
Rutas públicas
| Método | Path | Notas |
|---|---|---|
GET | /api/health | Healthcheck del servicio Nest |
POST | /api/users | Registro con PIN → sesión JWT |
POST | /api/auth/login | Login por PIN → token + expires_at |
Auth (protegidas)
| Método | Path | Notas |
|---|---|---|
POST | /api/auth/logout | Revoca sesión |
GET | /api/auth/me | Usuario actual |
Tareas (protegidas)
| Método | Path | Notas |
|---|---|---|
GET | /api/tasks | Lista del usuario autenticado |
POST | /api/tasks | Crear tarea |
POST | /api/tasks/bulk | Carga masiva |
PUT | /api/tasks/:id | Actualizar (ownership) |
DELETE | /api/tasks/:id | Borrar una |
DELETE | /api/tasks | Borrar todas del usuario |
Ownership
Las mutaciones validan user_id === auth.userId. No hay multi-tenant organizacional: cada PIN/usuario es un namespace de tareas.
06-auth.md
Modelo de autenticación
Flujo
- El usuario crea cuenta con un PIN de 8 dígitos (Options en la PWA).
- El servidor hashea el PIN (
bcryptjs+PIN_PEPPER) y guardapin_hash/pin_lookup. - Se crea una fila en
sessions(UUID) y se emite un JWT (jose, HS256) con claimssub(userId) ysid(sessionId). - El cliente guarda el token y lo envía en
Authorization: Bearer …. - Logout revoca la sesión; TTL configurable (
SESSION_TTL_SECONDS, default 24h).
Seguridad
- Rate-limit en endpoints de auth (por IP, en memoria por instancia de función).
DATABASE_URLnunca se expone al browser.- PIN no se almacena en claro.
Por qué PIN y no email/password
Onboarding en segundos para uso personal multi-dispositivo, sin buzones ni reset de contraseña, manteniendo hash + pepper + rate-limit.
07-data-model.md
Modelo de datos (Neon Postgres)
Tablas
users
idBIGSERIAL PKpin_hashTEXT NOT NULLpin_lookupTEXT (índice único parcial)created_attimestamptz
tasks
idBIGSERIAL PKuser_idBIGINT FK → users ON DELETE CASCADEtitleTEXT NOT NULLdescriptionTEXTdoneBOOLEAN DEFAULT falsepriorityTEXT DEFAULTmediumCHECK (low|medium|high)tagsTEXT[] DEFAULT{}created_at/updated_at(triggerhandle_updated_at)
sessions
idUUID PKuser_idBIGINT FKexpires_at/revoked_at/created_at
Índices
tasks(user_id),tasks(done),tasks(priority)- GIN en
tasks(tags) tasks(created_at DESC)sessions(user_id),sessions(expires_at)
Migraciones
SQL versionado en el repo: neon-migration.sql, neon-auth-migration.sql.
08-deploy-vercel.md
Deploy en Vercel (un solo proyecto)
Idea
apps/web→ estáticos en CDN (apps/web/www)apps/api→ función serverless víaapi/index.js(incluyeapps/api/dist/**)/api/*reescrito a la función; el resto a la SPA
Config (vercel.json)
buildCommand:npx turbo run build --filter=@task-cloud/web --filter=@task-cloud/apioutputDirectory:apps/web/wwwfunctions.api/index.js.maxDuration: 30- Rewrites:
/api/:path*→/api/index; resto →/index.html
Variables de entorno
| Nombre | Requerido | Notas |
|---|---|---|
DATABASE_URL | sí | Connection string Neon |
JWT_SECRET | sí | openssl rand -hex 32 |
PIN_PEPPER | sí | openssl rand -hex 32 |
SESSION_TTL_SECONDS | no | default 86400 |
API_BASE_URL | no | default /api (same origin) |
Smoke
GET https://task-manager-cristiancode.vercel.app/api/health- Abrir la web → Options → registrar PIN → crear/sync tareas
Local
npm install
cp .env.example .env # DATABASE_URL, JWT_SECRET, PIN_PEPPER
npm run api # :3001
npm start # :4200 con proxy /api 09-decisions.md
Decisiones
Neon + Nest en lugar de Supabase client
La nube es Postgres en Neon y una API Nest propia: SQL y esquema controlados, secretos solo en servidor, mismo patrón que el resto del portfolio. Se dejó el acoplamiento a Supabase Auth/Realtime porque el producto solo necesita PIN, sesiones y tareas.
Turborepo monorepo + un deploy
Web y API comparten lockfile, turbo cache y un solo pipeline Vercel. Evita dos proyectos desincronizados y simplifica same-origin /api.
Offline primero, sync opcional
Las features core de tareas no requieren API; la nube es continuidad multi-dispositivo, no el camino crítico de cada tap.
PIN en vez de email/password
Onboarding en segundos para uso personal, sin buzones ni flujos de reset, manteniendo hash + pepper + rate-limit.
Angular 22 con Zone (no zoneless) por Ionic
La skill Angular 22 pide zoneless, [formField] en todos los controles y View Transitions del router. Ionic 8.8 no lo permite: sigue pidiendo zone.js, sus ion-* no implementan FormValueControl y la navegación va por ion-router-outlet. Se adoptan Signal Forms + httpResource donde el DOM es nativo/Material; se documenta el techo en 11-angular-skill.md.
10-challenges-results.md
Retos y aprendizajes
Serverless Nest en Vercel
Adapter Express, cold start, maxDuration y reutilizar la app entre invocaciones warm (api/index.js).
Convergencia local ↔ nube
Mapear ids locales tras upload, bulk sync y no pisar ownership entre usuarios.
Env dual (web + api)
set-env.js genera environment.*.local.ts desde el .env raíz; secretos nunca van al bundle del browser.
Resultados
- Monorepo operativo con CI (lint, tests web, build API) en GitHub Actions.
- PWA en producción en Vercel (estáticos + API Nest en
/api) sobre Neon. - API con health, auth PIN/JWT, CRUD/bulk de tareas y esquema Neon versionado en SQL.
- Código abierto como referencia Angular/Ionic + Nest + Neon + Turborepo + Vercel.
11-angular-skill.md
Skill Angular
La guía de estilo Angular de Task Cloud está en .claude/skills/angular/.
| Archivo | Contenido |
|---|---|
SKILL.md | Defaults Angular 22 + house style |
style-standards.md | Signals first, visibilidad, const, spread, early returns |
signals.md | computed, effect, resource/httpResource, zoneless |
signal-forms.md | Signal Forms (estable en v22) |
components.md | Standalone, @if/@for, SSR, Vitest |
animations.md | animate.enter / View Transitions |
La skill pide mirar el major del repo y no mezclar eras. Task Cloud está en Angular 22.1 + Ionic 8.8.18 + TypeScript 6.0. v22 hace zoneless el default en apps nuevas; esta app declara provideZoneChangeDetection() porque Ionic 8 sigue apoyándose en Zone.js.
Revisión 2026-08-13 (Angular 22)
Hallazgos aplicados:
- Upgrade a Angular 22.1.1 + Material/CDK 22.1 + CLI 22.1. TypeScript 6.0.3. Ionic 8.8.18 (peer
>=16). - Signal Forms en crear y editar tarea:
form()+FormField+required/maxLength+submit(). El modelo es unsignalplano (taskModel/editModel).ion-selectymat-selectescriben el modelo (no tienen CVA de Signal Forms). httpResourcepara lecturas GET:cloudTasks(GET /tasks) ymeResource(GET /auth/me), disparados porsession. Mutaciones (POST/PUT/DELETE) siguen enHttpClient; tras mutar se llamacloudTasks.reload().- HTTP Fetch,
@if, OnPush y Zone explícito se mantienen.
Qué Angular 22 no se puede (o no se debe) aplicar por Ionic 8
Ionic 8.8 declara zone.js como peer y sus web components (ion-select, overlays, ion-router-outlet, tabs) no implementan las APIs signal-first de v22. Esto no es deuda de la skill: es un techo del runtime Ionic. No forzar estas APIs hasta que Ionic las soporte.
| API Angular 22 | Por qué Ionic la bloquea | Qué hacemos aquí |
|---|---|---|
Zoneless (provideZonelessChangeDetection(), quitar zone.js) | Peer oficial zone.js >= 0.13. Eventos, overlays y navegación de Ionic siguen notificando la vista vía Zone. Sin Zone, taps y modales se quedan mudos. | provideZoneChangeDetection() + zone.js |
[formField] en controles Ionic | ion-select, ion-checkbox, ion-toggle, ion-input no implementan FormValueControl. Signal Forms solo enlaza nativos / Material. | form() + [formField] en matInput; ion-select / mat-select escriben el signal del modelo |
| View Transitions del router Angular | La navegación real pasa por ion-router-outlet + IonicRouteStrategy, no por las transiciones del Router de Angular. | Transiciones de Ionic; no withViewTransitions() |
| Angular ARIA en overlays | Alertas, popovers y action sheets son de Ionic. Sustituirlos por Angular ARIA rompe el look y el focus trap nativo. | Overlays Ionic; ARIA solo si se añade un control nuevo no-Ionic |
| SSR / hidratación | PWA + Capacitor + IonicStorage son cliente. No hay servidor de render para ion-*. | SPA + service worker |
No atribuir a Ionic (queda fuera por el builder/legado, no por el peer):
- Vitest — el builder webpack/Karma de esta app
- Shell
NgModule(IonicModule.forRoot(),TabsModule) — Ionic 8 sí tieneprovideIonicAngular(); no se ha migrado el andamiaje de tabs
Revisar esta tabla al subir de major de Ionic. Si el peer deja de pedir zone.js y los ion-* implementan FormValueControl, se puede zoneless + [formField] en selects.
House style (extracto)
- Estado y derivación en
signal/computed/linkedSignal/httpResource. RxJS solo para streams reales; al entrar,toSignal(). private readonlypor defecto;protectedsolo si la plantilla lo lee.constpara locales; nuncavar.- Arrays/objetos por spread; nunca
push/splicesobre estado compartido. - Guards arriba, happy path plano.
- En código nuevo: standalone,
inject(),input()/output(), Signal Forms,httpResourcepara lecturas.
Referencia en el repo de la app
Los agentes que lean .claude/skills/angular/SKILL.md aplican estas reglas al tocar apps/web.