Backend de la Plataforma de Reservas de Servicios (caso CodeF@ctory). Monolito modular con arquitectura limpia sobre Spring Boot 4.1.1 (JDK 21), PostgreSQL administrado en Supabase y autenticación delegada en Supabase Auth (GoTrue) con validación JWT por JWKS.
Alcance Sprint 1: HU-001 (registro de cliente con verificación de correo) y HU-021 mínima (login, logout, bloqueo por intentos, recuperación de contraseña). MFA de administradores diferido a Sprint 2.
| Documento | Ruta |
|---|---|
| Diagrama de paquetes y componentes + estilo | docs/architecture/componentes.md |
| ADR-0001 Monolito modular + clean architecture | docs/adr/ADR-0001-*.md |
| ADR-0002 Supabase (BD + identidad) | docs/adr/ADR-0002-*.md |
| ADR-0003 Autenticación JWT/JWKS + bloqueo propio | docs/adr/ADR-0003-*.md |
| ADR-0004 API versionada, ProblemDetail, traceId | docs/adr/ADR-0004-*.md |
| ADR-0005 Modelo físico escrito a mano + CI de BD | docs/adr/ADR-0005-*.md |
| ADR-0006 Migraciones de esquema con Flyway | docs/adr/ADR-0006-*.md |
| ADR-0007 Correo de la plataforma: bandeja en la app, SMTP opcional | docs/adr/ADR-0007-*.md |
| ADR-0008 Autorización: rol de plataforma y rol en el proveedor | docs/adr/ADR-0008-*.md |
| Guía de la carpeta de base de datos | docs/database/README.md |
| Modelo lógico completo (ER) | docs/database/modelo-logico.md |
| Modelo físico: migraciones de Flyway (V1 = las 19 tablas del Sprint 1) | src/main/resources/db/migration/ |
| Datos de prueba | docs/database/seed.sql |
| Restricciones de integridad en acción | docs/database/pruebas-integridad.sql |
| Seguridad Supabase (RLS) | src/main/resources/db/migration/V2__rls.sql |
| Consultas clave del negocio | docs/database/consultas-clave.md |
| Consultas clave, versión ejecutable | docs/database/consultas-clave.sql |
| Diagrama ER editable | docs/database/diagrama-er.drawio |
| Runbook operativo (producción, E2E, troubleshooting) | docs/operations/runbook.md |
| Colección REST de Sprint 1 | docs/api/sprint1.http |
com.codefactory.bookingplatform
├── shared/ kernel transversal: error (ProblemDetail+traceId), config
│ (security, OpenAPI, properties), observability, persistence
├── identity/ HU-001: domain (Client puro + ports) → application (use cases)
│ → infrastructure (JPA + MapStruct) → api (REST /registrations)
├── auth/ HU-021: domain (policies + ports) → application (fachada
│ UserProvisioning + use cases) → infrastructure (GoTrueClient,
│ login_attempts) → api (REST /auth)
├── catalog/ HU-004, HU-009: domain (OfferedService, Resource con mantenimientos y
│ retiro) → application (use cases, ámbito vía OrganizationAccess) →
│ infrastructure (JPA + JDBC) → api (REST /organizations/{id}/services,
│ /resource-types, /resources)
├── scheduling/ HU-006: domain (Agenda con bloques, vigencia y granularidad) →
│ application (use cases; reservas futuras vía puerto) → infrastructure
│ (JPA + JDBC) → api (REST /organizations/{id}/agendas)
├── organization/ HU-002: domain (Organization + política versionada) → application
│ (registro, aprobación, OrganizationAccess) → infrastructure (JPA)
│ → api (REST /provider-registrations, /organizations, /admin)
└── notification/ correo de la plataforma: fachada EmailNotifications → bandeja en la
app (GET /notifications/emails) o SMTP si se configura (ADR-0007)
Reglas de dependencia verificadas con ArchUnit en src/test/.../architecture/ArchitectureTest.java.
- JDK 21+ (el Maven wrapper
mvnwya viene incluido, no hay que instalar Maven) - Docker Desktop (solo para los tests de integración con Testcontainers)
- Proyecto Supabase gratuito (BD + Auth)
Notas:
- No hay base de datos local que levantar: la app conecta directo al PostgreSQL administrado de Supabase por el session pooler (puerto 5432).
- La aplicación solo lee configuración del entorno del proceso (variables de entorno del SO, secret store de Render, etc.).
| Variable | Valor de ejemplo (reemplazar) | Descripción |
|---|---|---|
SUPABASE_URL |
https://<ref>.supabase.co |
URL del proyecto (Dashboard → Settings → API Keys) |
SUPABASE_SECRET_KEY |
sb_secret_... |
Secret key, solo backend, jamás en frontend ni git |
DATABASE_URL |
jdbc:postgresql://aws-0-<region>.pooler.supabase.com:5432/postgres?sslmode=require |
JDBC por session pooler, puerto 5432 (nunca 6543) |
DATABASE_USER |
postgres.<ref> |
Usuario del pooler (con sufijo del proyecto) |
DATABASE_PASSWORD |
... |
Contraseña de la BD (Dashboard → Settings → Database) |
SPRING_MAIL_HOST |
smtp.gmail.com |
Opcional, para local. Sin ella los correos van a la bandeja de la app (ADR-0007). Render gratis bloquea SMTP |
SPRING_MAIL_PORT |
587 |
Puerto SMTP con STARTTLS |
SPRING_MAIL_USERNAME |
cuenta@gmail.com |
Usuario SMTP |
SPRING_MAIL_PASSWORD |
... |
Contraseña de aplicación de Gmail, no la de la cuenta |
APP_MAIL_FROM |
reservas@... |
Opcional. Remitente; si falta, el usuario SMTP |
La app no lee archivos .env por sí sola: las variables deben existir
en el entorno de la terminal donde corres mvnw. Cada terminal tiene su
propio entorno, así que cárgalas en la misma sesión antes de arrancar.
Opción A — una por una (nada persiste, solo esa sesión):
Windows:
$env:SUPABASE_URL="https://<ref>.supabase.co"
$env:SUPABASE_SECRET_KEY="sb_secret_..."
$env:DATABASE_URL="jdbc:postgresql://aws-0-<region>.pooler.supabase.com:5432/postgres?sslmode=require"
$env:DATABASE_USER="postgres.<ref>"
$env:DATABASE_PASSWORD="..."Linux/macOS:
export SUPABASE_URL="https://<ref>.supabase.co"
export SUPABASE_SECRET_KEY="sb_secret_..."
export DATABASE_URL="jdbc:postgresql://aws-0-<region>.pooler.supabase.com:5432/postgres?sslmode=require"
export DATABASE_USER="postgres.<ref>"
export DATABASE_PASSWORD="..."Opción B — desde un archivo .env (recomendado):
Crea un archivo .env en la raíz del repo con el formato CLAVE=valor,
una por línea (ese archivo ya está ignorado en git, nunca se commitea):
SUPABASE_URL=https://<ref>.supabase.co
SUPABASE_SECRET_KEY=sb_secret_...
DATABASE_URL=jdbc:postgresql://aws-0-<region>.pooler.supabase.com:5432/postgres?sslmode=require
DATABASE_USER=postgres.<ref>
DATABASE_PASSWORD=...
Y cárgalo en la misma terminal antes de arrancar:
Windows:
Get-Content .env | Where-Object { $_ -match '^\s*[^#\s=]+=' } | ForEach-Object { $k,$v = $_ -split '=', 2; [Environment]::SetEnvironmentVariable($k.Trim(), $v.Trim().Trim('"').Trim("'"), 'Process') }Linux/macOS:
set -a; source .env; set +aSi algo falla al arrancar, verifica que cargaron (p. ej. $env:DATABASE_URL
en Windows o echo $DATABASE_URL en Linux/macOS).
Windows:
.\mvnw.cmd spring-boot:runLinux/macOS:
./mvnw spring-boot:runNotas:
- El esquema lo crea Flyway al arrancar, con las migraciones de
src/main/resources/db/migration/(ADR-0006). Hibernate ya no crea tablas: solo valida que las entidades cuadren con el esquema. - La aplicación migra la base a la que apunta
DATABASE_URL. Si tu.envapunta a Supabase, al arrancar le aplicas las migraciones de tu rama a la base compartida. Para desarrollar, usa el PostgreSQL dedocker compose up -d db(DATABASE_URL=jdbc:postgresql://localhost:5432/postgres). - Swagger UI: http://localhost:8080/swagger-ui.html
- Health: http://localhost:8080/actuator/health
Windows:
.\mvnw.cmd test # unitarios de dominio + ArchUnit
.\mvnw.cmd verify # + integración con Testcontainers (requiere Docker)Linux/macOS:
./mvnw test # unitarios de dominio + ArchUnit
./mvnw verify # + integración con Testcontainers (requiere Docker)| Método | Ruta | Auth | HU |
|---|---|---|---|
| POST | /api/v1/registrations |
pública | HU-001 |
| POST | /api/v1/registrations/verification-resends |
pública | HU-001 |
| POST | /api/v1/registrations/email-verifications |
pública | HU-001 |
| POST | /api/v1/auth/login |
pública | HU-021 |
| POST | /api/v1/auth/logout |
Bearer | HU-021 |
| POST | /api/v1/auth/password-recovery-requests |
pública | HU-021 |
| POST | /api/v1/auth/password-resets |
pública | HU-021 |
| GET | /api/v1/auth/me |
Bearer | HU-021 |
| GET | /api/v1/organization-categories |
pública | HU-002 |
| GET | /api/v1/cities |
pública | HU-002 |
| POST | /api/v1/provider-registrations |
pública | HU-002 |
| GET | /api/v1/catalog/services?q=&categoryId=&cityId=&providerId=&minPrice=&maxPrice=&sort=&direction=&page=&size= |
pública | HU-005 |
| GET | /api/v1/organizations |
Bearer PROVIDER |
HU-002 |
| GET | /api/v1/organizations/{id} |
Bearer PROVIDER miembro o ADMIN |
HU-002, HU-022 |
| PUT | /api/v1/organizations/{id}/operation-policy |
Bearer OWNER o MANAGER del proveedor |
HU-002 |
| POST | /api/v1/admin/organizations/{id}/approval |
Bearer ADMIN |
HU-002 |
| GET | /api/v1/admin/organizations, /api/v1/admin/organizations/export (CSV) |
Bearer ADMIN |
HU-003 |
| POST | /api/v1/admin/organizations/{id}/suspension, /reactivation, /rejection |
Bearer ADMIN |
HU-003 |
| GET | /api/v1/admin/clients, /api/v1/admin/clients/export (CSV) |
Bearer ADMIN |
HU-003 |
| POST | /api/v1/admin/clients/{id}/suspension, /reactivation |
Bearer ADMIN |
HU-003 |
| GET | /api/v1/notifications/emails |
Bearer | HU-002 (correo de aprobación), ADR-0007 |
| GET, POST | /api/v1/organizations/{id}/resource-types |
Bearer miembro (POST: OWNER o MANAGER) |
HU-009 |
| GET, POST | /api/v1/organizations/{id}/resources |
Bearer miembro (POST: OWNER o MANAGER) |
HU-009 |
| POST | /api/v1/organizations/{id}/resources/{resourceId}/maintenances |
Bearer OWNER o MANAGER |
HU-009 |
| POST | /api/v1/organizations/{id}/resources/{resourceId}/retirement |
Bearer OWNER o MANAGER |
HU-009 |
| GET, POST | /api/v1/organizations/{id}/services |
Bearer miembro (POST: OWNER o MANAGER) |
HU-004 |
| GET, PUT, DELETE | /api/v1/organizations/{id}/services/{serviceId} |
Bearer miembro (PUT, DELETE: OWNER o MANAGER) |
HU-004 |
| POST | /api/v1/organizations/{id}/services/{serviceId}/activation, /deactivation |
Bearer OWNER o MANAGER |
HU-004, HU-009 |
| GET, POST | /api/v1/organizations/{id}/agendas |
Bearer miembro (POST: OWNER o MANAGER) |
HU-006 |
| GET | /api/v1/organizations/{id}/agendas/{agendaId} |
Bearer miembro | HU-006 |
| PUT | /api/v1/organizations/{id}/agendas/{agendaId}/blocks, /granularity |
Bearer OWNER o MANAGER |
HU-006 |
| POST | /api/v1/organizations/{id}/agendas/{agendaId}/day-copies |
Bearer OWNER o MANAGER |
HU-006 |
Errores: application/problem+json con errorCode, traceId y details.
Producción: https://bookingplatform-81wi.onrender.com
(Web Service Docker desde main, perfil cloud. Al arrancar, Flyway aplica
las migraciones pendientes a Supabase y Hibernate valida el esquema).
- En Render: New → Web Service → Build from Git provider y conecta el
repositorio (
render.yamlya describe el servicio). - En Settings → Health Check Path configura
/actuator/health/readiness. - En Environment define las mismas variables de la tabla anterior
(perfil
cloud;DATABASE_URLen formato JDBC con?sslmode=require). Sin Secret Files: la app solo lee variables de entorno.
curl https://bookingplatform-81wi.onrender.com/actuator/health # {"status":"UP"}Swagger UI: /swagger-ui/index.html · OpenAPI: /v3/api-docs.
Plan Free: la instancia se duerme tras ~15 min sin tráfico; la primera petición tarda ~1 min (cold start).
- Render inyecta el puerto en
PORT→ respetarserver.port=${PORT:8080}. - El session pooler gratuito de Supabase admite 15 clientes totales:
pool Hikari fijo en 5/1 (
application-cloud.yml) y ninguna otra conexión permanente contra Supabase.
Detalle operativo, E2E manual y troubleshooting:
docs/operations/runbook.md.
- Clases/interfaces/records/enums: PascalCase; métodos y variables: camelCase; paquetes: minúsculas.
- Sufijos:
*UseCase(application),*Entity(JPA, infrastructure),*Request/*Response(DTO api),*Port/*Repository(domain),*Adapter/*Client(infrastructure). - DTOs de entrada/salida como
recordcon Bean Validation. - Dominio sin frameworks (regla verificada por ArchUnit).
- Ramas
feature/*, PR amaincon revisión por pares (trunk-based).