Implementación¶
Disponible solo para: Platform Admin
Hardhat Flow está desplegado en DigitalOcean App Platform. Toda la infraestructura se administra como código — no se permiten cambios manuales en producción.
Panorama de infraestructura¶
| Component | Technology | Provider |
|---|---|---|
| Alojamiento de la aplicación | DigitalOcean App Platform | DigitalOcean |
| Servidor web | Gunicorn | (incluido en el contenedor de la app) |
| Archivos estáticos | WhiteNoise | (servidos desde el proceso de la app) |
| Base de datos | Clúster MySQL administrado | DigitalOcean |
| Tareas en segundo plano | Celery + Redis | DigitalOcean (Redis administrado) |
| Almacenamiento de archivos | Spaces (almacenamiento tipo S3) | DigitalOcean |
| Autenticación | Auth0 (validación JWT) | Auth0 |
Servidor de aplicación¶
La aplicación Django corre detrás de Gunicorn con varios procesos worker.
- Los archivos estáticos los sirve directamente el proceso de la app con WhiteNoise — no hace falta nginx ni CDN separado para activos estáticos.
- El número de workers de Gunicorn se configura con la variable de entorno
WEB_CONCURRENCY. App Platform de DigitalOcean la establece automáticamente según el tamaño de instancia.
Enrutamiento de URL en App Platform¶
Las reglas de ingreso en .do/app.yaml envían prefijos de URL distintos al sitio estático Next.js o al servicio web Django:
/— SPA (Dashboard, Bids, Jobs, …)./api/,/healthz,/metrics,/docs/— Django./platform/— aprovisionamiento de inquilinos y APIs REST de plataforma (/platform/tenants/, …)./django-admin/— consola de administración de base de datos de Django (staff/superusuario)./platform-admin/— UI Platform Admin en la aplicación (Tenants, Users, Access Requests).
Como la SPA vivía antes bajo /admin/, ese prefijo entraba en conflicto con el admin de contrib de Django cuando ambos iban a Django. En producción se usa /platform-admin/ para la UI del producto y /django-admin/ para Django admin.
Base de datos¶
Cada inquilino tiene una base de datos MySQL dedicada en el clúster MySQL administrado de DigitalOcean.
- El clúster maneja respaldos automáticos, conmutación por error y parches.
- Una base de datos de plataforma separada en el mismo clúster guarda registros de inquilinos y credenciales cifradas.
- La aplicación nunca se conecta directamente por nombre de host a la BD del inquilino — siempre enruta mediante el enrutador de BD usando credenciales obtenidas de la base de plataforma.
Respaldos¶
- MySQL administrado de DigitalOcean ofrece respaldos diarios automáticos con ventana de retención configurable para el clúster (incluida la base de plataforma).
- Volcados nocturnos automatizados de cada inquilino a las 2:00 AM en la zona horaria configurada de la aplicación (
TIME_ZONE/ zona de Celery beat, actualmente America/Los_Angeles) mediante la tarea programada Celery Beatbackup_all_tenant_dbs. - Un pase de retención posterior corre a las 3:30 AM en esa misma zona mediante
prune_all_tenant_backups, para que el recorte siga ejecutándose aunque una tarea de volcado individual no terminó el paso de poda. - Cada base activa de inquilino se vuelca con
mysqldump --single-transactiony se almacena como.sql.gzcomprimido con gzip. - Los archivos de respaldo están en DigitalOcean Spaces en
backups/{tenant_slug}/{YYYYMMDD-HHMMSS}.sql.gz. El prefijoYYYYMMDDes el día calendario del snapshot enTIME_ZONE. Los respaldos entre medianoche y 5:59 AM local usan el día calendario anterior como prefijo para alinear el trabajo nocturno con el “día anterior”. - Retención (Spaces): por carpeta de inquilino la aplicación conserva seis objetos cuando existen esos días en almacenamiento — uno cada uno para: los cuatro días previos (D-1 … D-4), el archivo de hace siete días (ranura semanal), y el mismo día calendario del mes anterior (ranura mensual; si ese día no existe, ej. 31 de marzo → febrero, se usa el último día válido de ese mes). Si hay más de un volcado el mismo día calendario, se conserva el de marca de tiempo más reciente ese día. Todo lo demás bajo
backups/{tenant_slug}/que coincida con*.sql.gzse elimina. - Si un respaldo falla, Celery reintenta hasta 3 veces con 5 minutos de espera. Los fallos se registran como eventos JSON estructurados. Si la poda falla tras una carga exitosa, el fallo se registra pero el nuevo archivo de volcado permanece en Spaces.
Restaurar la base de un inquilino sobrescribe el estado actual. La operación es irreversible. Confirme siempre la intención y documente el motivo antes de iniciar una restauración.
Tareas en segundo plano¶
Los workers Celery manejan todas las operaciones asíncronas:
- Entrega SMS (Twilio)
- Conversión de fotos HEIC
- Generación de miniaturas
- Aprovisionamiento de bases de inquilinos
- Tareas de mantenimiento programadas
Redis (Redis administrado de DigitalOcean) es el broker y backend de resultados de Celery.
Los workers Celery corren como proceso/servicio separado en App Platform junto al proceso web.
Si los workers Celery están caídos, las tareas asíncronas se acumulan en cola pero no se procesan. Las notificaciones SMS, conversiones de fotos y generación de miniaturas se retrasarán hasta que los workers reinicien.
Almacenamiento de archivos¶
Los archivos cargados por usuarios (fotos) se guardan en DigitalOcean Spaces, servicio de almacenamiento de objetos compatible con S3.
- Los archivos de cada inquilino están separados en el bucket por slug del inquilino.
- Se usan URLs firmadas para servir archivos — no son públicos por defecto.
- Los archivos eliminados de forma lógica se conservan en Spaces hasta que un Platform Admin los retire explícitamente.
Autenticación¶
Auth0 emite JWT al iniciar sesión. La aplicación valida tokens en cada solicitud API:
- La firma del token se verifica contra el endpoint JWKS de Auth0.
- Se inspeccionan reclamos para rol y contexto de inquilino.
- No se usan cookies de sesión — la API es totalmente sin estado.
- La caducidad y renovación del token las maneja el frontend/cliente.
La configuración de Auth0 (dominio, audiencia, ID de cliente) se inyecta por variables de entorno. Vea la sección Configuración de entorno más abajo.
Las claves JWKS se obtienen de Auth0 y se guardan en caché varios minutos (configurable con AUTH0_JWKS_CACHE_TTL_SEC, predeterminado 300). Cada obtención usa un timeout HTTP corto (AUTH0_JWKS_FETCH_TIMEOUT_SEC, predeterminado 5 segundos) para que los workers no se bloqueen si Auth0 está lento o inalcanzable.
Resolver problemas de autenticación y APIs de plataforma¶
Si las páginas Platform Admin (por ejemplo Access Requests) cargan indefinidamente o devuelven errores:
- Confirme HTTPS saliente desde Django a
https://<AUTH0_DOMAIN>/.well-known/jwks.json(firewall, proxy de egreso, VPC). Sin eso falla la verificación del token y las APIs devuelven 401. - Revise
NEXT_PUBLIC_API_URLen el build del frontend: debe ser el mismo origen público que los usuarios usan para la API Django (vea Enrutamiento de ingreso). Un origen incorrecto envía llamadas del navegador al host equivocado o rompe CORS. - Revise variables de build Auth0 del frontend (
NEXT_PUBLIC_AUTH0_DOMAIN,NEXT_PUBLIC_AUTH0_CLIENT_ID,NEXT_PUBLIC_AUTH0_AUDIENCE). Valores faltantes pueden dejar usuarios en Redirecting to login.... - Si un valor
NEXT_PUBLIC_*del frontend comienza conEV[(ej.EV[1:...]), App Platform recibió un marcador cifrado en lugar del secreto real. Reemplácelo con el valor Auth0/API real en variables de entorno BUILD_TIME del frontend y vuelva a desplegar. - En Auth0 Allowed Callback URLs, incluya la ruta completa
/post-login/(ej.https://hardhatpro.com/post-login/yhttp://localhost:3000/post-login/). El callback SPA de Hardhat Flow es/post-login/.
Configuración de entorno¶
Todos los secretos y la configuración se inyectan como variables de entorno. No se guardan credenciales en texto plano en el repositorio.
Categorías principales:
| Category | Variables |
|---|---|
| Django | SECRET_KEY, DEBUG, ALLOWED_HOSTS, DJANGO_LOG_LEVEL |
| Base de datos | PLATFORM_DB_URL (cadena de conexión a la BD de plataforma) |
| Auth0 | AUTH0_DOMAIN, AUTH0_AUDIENCE, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, opcional AUTH0_JWKS_CACHE_TTL_SEC, AUTH0_JWKS_FETCH_TIMEOUT_SEC |
| Celery / Redis | CELERY_BROKER_URL, CELERY_RESULT_BACKEND |
| Almacenamiento | DO_SPACES_KEY, DO_SPACES_SECRET, DO_SPACES_BUCKET, DO_SPACES_REGION |
| Twilio | TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN |
Nunca codifique credenciales en archivos de settings, Dockerfiles ni código fuente. Todos los secretos deben provenir de variables de entorno o un gestor de secretos.
Repositorio de GitHub movido a una nueva organización¶
Cuando el repositorio se transfiere (por ejemplo de una cuenta personal a GV5-Tech/hardhat-flow), los despliegues pueden fallar con errores como:
GitHub app does not have access to repoGitHub user does not have access to Org/repo(al actualizar el spec de la app condoctl)
App Platform sigue usando la ruta antigua owner/repo guardada en la app en vivo hasta que la actualice. La copia en este repositorio (.do/app.yaml) puede ya mostrar la nueva organización mientras DigitalOcean aún apunta al propietario anterior.
Por qué no puede cambiar el repo en “Source” de cada componente¶
En App Platform, la configuración Source por componente no es donde se cambia el repositorio de GitHub. Esa pantalla solo permite ajustar rama, directorio fuente y autodespliegue; el repositorio aparece como enlace y no puede cambiarse ahí.
DigitalOcean documenta que cambiar el repositorio conectado se hace actualizando el spec de la app (cada campo github.repo), no mediante ese editor Source — vea How to Manage an App’s Source Repository.
Use Apps → hardhat-flow → Settings, desplácese a App Spec, Edit, y cambie cada repo: bajo un bloque github: (web, frontend, worker, beat, migrate) a GV5-Tech/hardhat-flow, luego guarde. También puede descargar el spec, editar localmente y subirlo de nuevo. Guardar puede disparar un redespliegue.
1. Conectar la GitHub App de DigitalOcean a la nueva organización¶
Un propietario de la organización (o administrador, según política) debe conceder acceso:
-
Asegúrese de que la GitHub App de DigitalOcean pueda alcanzar
GV5-Tech/hardhat-flow. Vincule o actualice la conexión GitHub en DigitalOcean → Account settings → GitHub si hace falta, y en GitHub use OrganizationGV5-Tech→ Settings → GitHub Apps (o Third-party access), busque DigitalOcean y Configure para que el acceso al repositorio incluyahardhat-flow(o todos los de la organización, según su política). -
Confirme que la cuenta GitHub vinculada en DigitalOcean → Account → GitHub puede acceder a
GV5-Tech/hardhat-flow(miembro con lectura basta para builds).
Hasta que este paso funcione, doctl apps update y doctl apps create-deployment pueden seguir fallando con errores de acceso a GitHub.
2. Apuntar cada componente al nuevo path del repositorio¶
Tras que GitHub funcione, alinee el spec en vivo de la app con GV5-Tech/hardhat-flow:
doctl apps spec get <app-id> -o yaml > live-spec.yaml
Edite cada github.repo (web, frontend, worker, beat, migrate) del antiguo owner/hardhat-flow a GV5-Tech/hardhat-flow, luego:
doctl apps update <app-id> --spec live-spec.yaml
Si prefiere aplicar el spec canónico del repositorio (y sus secretos ya están en el panel), puede usar .do/app.yaml — asegúrese de que cada github.repo sea GV5-Tech/hardhat-flow.
3. Desplegar¶
doctl apps create-deployment <app-id>
CI/CD¶
Todos los despliegues a producción pasan por la canalización CI/CD. No se permiten cambios manuales en producción.
La canalización:
- Ejecuta
ruff check .yruff format --check . - Ejecuta
pytest --cov=apps --cov-report=xml - Si pasa, construye la imagen de la aplicación.
- Despliega a DigitalOcean App Platform vía API de DO.
- Ejecuta
python manage.py migrate --no-inputcomo paso previo al despliegue.
No use la consola de DigitalOcean ni doctl para empujar código o configuración manualmente a producción. Todo cambio pasa por CI.
Los fallos de migración en el paso previo al despliegue abortan el despliegue y dejan la versión actual en ejecución. Pruebe siempre migraciones localmente antes de fusionar.
Spec de App Platform¶
La configuración completa de DigitalOcean App Platform está definida en .do/app.yaml en la raíz del repositorio. Declara tres servicios (web, worker, beat) y un trabajo previo al despliegue (migrate).
Enrutamiento de ingreso¶
El ingreso de App Platform debe enviar el tráfico /platform al componente web (Django), no al sitio estático de marketing. El formulario público Request Access del sitio de marketing envía a /platform/access-requests/; si esa ruta la sirve el sitio estático, los envíos fallan. El spec del repositorio enruta /platform a web con preserve_path_prefix: true, junto con /api, /admin, /healthz, /metrics y /docs. Tras cambiar el enrutamiento, aplique el spec actualizado con doctl apps update <app-id> --spec .do/app.yaml.
Al construir la SPA, establezca NEXT_PUBLIC_API_URL al origen público del servicio web Django solamente (sin sufijo /api). La aplicación llama ${NEXT_PUBLIC_API_URL}/platform/... para APIs de plataforma.
Para desplegar una nueva instancia manualmente:
doctl apps create --spec .do/app.yaml
Para actualizar una app existente tras cambios en el spec:
doctl apps update <app-id> --spec .do/app.yaml
Todos los valores sensibles (secretos, cadenas de conexión) deben establecerse como variables de entorno cifradas en el panel de App Platform tras la creación inicial — se referencian en el spec con type: SECRET pero no se guardan en el archivo del spec.