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 20 + 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 20, 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 20 + Ionic 8 + Tailwind. Servicios de dominio:
TaskService,TaskNeonService,NeonApiService. - 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.
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 · claude-workflow
Task Cloud adopta la skill angular del repo interno claude-workflow (.claude/skills/angular). Vive en el monorepo 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 20 + Ionic 8 (Ionic 8 soporta hasta Angular 20.x). Se aplicó el house style y las novedades de v20 que encajan con Ionic; se dejaron las APIs estables solo en v21/v22.
Qué se aplicó en apps/web
ng updatea Angular 20.3 y Material/CDK 20. TypeScriptmoduleResolution: bundler.- Control flow nativo. Migración de
*ngIfrestantes a@if(lista, PIN dialog). - HTTP moderno. Se quitó el deprecado
HttpClientModule;provideHttpClient(withFetch(), withInterceptors(...))usa el backend Fetch de Angular 20. - OnPush en
TabListPage,TabOptionsPage, diálogo de edición y PIN dialog (alineado con el default implícito de majors posteriores). - Signals first. PIN dialog:
copied/confirmedcomo signals.TaskService.storageReadyen vez deBehaviorSubject. Diálogo de edición confirstValueFrom. - Formularios. Sigue
ReactiveFormsModule: Signal Forms aún no son el default estable de v20 (lo son en v22). Ionic sigue usandoNgModuleen el shell.
Qué queda fuera a propósito (Ionic + v20)
- Signal Forms /
formField(estable en Angular 22) httpResource()/resource()siguen experimentales en 20.3- Zoneless (
provideExperimentalZonelessChangeDetection) — Ionic 8 + zone.js es el camino soportado - Vitest (Karma sigue en este builder)
ng updatea 21+ no cabe: Ionic 8 declara máximo Angular 20.x
House style (extracto)
- Estado y derivación en
signal/computed/linkedSignal. RxJS solo para streams reales (debounce, websockets, races); al entrar,toSignal(). private readonlypor defecto;protectedsolo si la plantilla lo lee; público es la excepción.constpara locales; nuncavar.- Arrays/objetos por spread (
[...xs, x],{ ...o, k },toSorted()), nuncapush/splicesobre estado compartido. - Guards arriba, happy path plano.
- En código nuevo: standalone,
inject(),input()/output(),@if/@for, no NgModules ni@Input().
Referencia en el repo de la app
Tras clonar task-manager-cloud, los agentes que lean .claude/skills/angular/SKILL.md aplican estas reglas al tocar apps/web.