
Un dominio, dos mundos: Angular en Firebase Hosting y NestJS en Cloud Run
Tabla de Contenidos
Tabla de Contenidos
El problema
Tengo una aplicación con dos mundos. El frontend es Angular y quiero servirlo desde Firebase Hosting: CDN global, HTTPS automático y deploys atómicos sin administrar un servidor. El backend es NestJS, y ahí Firebase no me alcanza: necesito un proceso Node corriendo, con su contenedor, sus variables de entorno y sus secretos.
La respuesta fácil sería tomar el API y llevarlo a cualquier VPS o a un servicio tipo Railway. Pero hay una decisión de fondo que cambia todo: en el roadmap de esta aplicación está integrar agentes con Vertex AI (Agent Platform). Eso significa que el backend tiene que vivir en Google Cloud, en el mismo proyecto, con la misma IAM y las mismas service accounts que después van a hablar con los modelos. Elegir dónde desplegar el API hoy es, en realidad, elegir qué tan doloroso será conectarlo con Vertex AI mañana.
Con esa restricción, Cloud Run es la opción natural: corre contenedores, escala a cero cuando nadie lo usa, y queda a una llamada de IAM de cualquier servicio de GCP.
Queda el tercer requisito, y es el que define este post: no quiero desplegar nada a mano. Un push a main tiene que disparar Cloud Build, y Cloud Build tiene que encargarse de todo: construir el API, publicar la imagen, actualizar Cloud Run, compilar el Angular y subirlo a Firebase Hosting. En ese orden, porque el frontend nuevo no puede llegar antes que el API que lo respalda.
Y hay un cuarto requisito que no negocio: los secretos. La URL de la base de datos, el secreto con el que firmo los JWT, las API keys de terceros. Nada de eso puede vivir en el repo, ni en un .env que viaja por Slack, ni pegado a mano en la consola de Cloud Run. Para esto GCP tiene Secret Manager: los secretos viven ahí, versionados y con su propio control de acceso por IAM, el API los recibe al arrancar y el pipeline los consume sin que aparezcan en el código ni en los logs.
Un detalle más que quiero gratis: el frontend y el API deben compartir dominio. midominio.com sirve el Angular y midominio.com/api llega a Cloud Run. Sin CORS, sin segundo certificado, sin exponer una URL *.run.app en el código del cliente.
Eso es lo que vamos a armar.
Las piezas: quién hace qué
Antes de tocar configuración, el mapa. La aplicación se divide en dos mitades con responsabilidades muy distintas, y entender esa frontera es lo que hace que el resto del post tenga sentido.
El frontend: Angular en Firebase Hosting
Cuando compilas Angular para producción, el resultado es una carpeta de archivos estáticos: HTML, JavaScript con hash en el nombre, CSS, fuentes, imágenes. No hay servidor, no hay Node en runtime. Son archivos y nada más.
Firebase Hosting hace tres cosas con ellos:
- Los sirve desde un CDN global. Tu
index.htmly tus bundles salen del edge más cercano al usuario, no de un servidor tuyo. HTTPS incluido, certificado incluido. - Cachea agresivo lo que puede. Los bundles de Angular llevan hash en el nombre (
main-K2QHTFXQ.js), así que pueden cachearse por un año conimmutable: si el contenido cambia, cambia el nombre. Elindex.htmlno se cachea, porque es el que apunta a los bundles nuevos. - Enruta. Aquí está la magia: Firebase Hosting no solo sirve archivos, también decide qué hacer con cada request. Las rutas de la SPA caen al
index.htmlpara que el router de Angular haga su trabajo. Y las que empiezan con/api/**ni siquiera tocan los estáticos: Firebase las reenvía directo a Cloud Run.
El frontend, entonces, es tonto a propósito. No sabe dónde vive el API. Llama a /api/members como si fuera un archivo más del mismo dominio, y Firebase se encarga del resto.
El backend: NestJS en Cloud Run
El API es lo contrario: un proceso vivo. NestJS compilado a JavaScript, corriendo sobre Node, escuchando un puerto. Para Cloud Run eso se empaqueta en una imagen Docker, y a partir de ahí Cloud Run se encarga de lo que en un VPS sería tu problema:
- Ejecuta el contenedor y escala según tráfico. Si llegan mil requests, levanta más instancias. Si no llega nadie, escala a cero y dejas de pagar. Para un side project o una app interna, esto cambia la ecuación de costos por completo.
- Inyecta la configuración. Las variables de entorno se declaran en el deploy, y los secretos llegan desde Secret Manager: el contenedor arranca con
DATABASE_URLoJWT_SECRETen su entorno sin que existan en la imagen ni en el repo. - Le da al servicio una identidad. Cada servicio de Cloud Run corre con una service account. Hoy le da permisos para leer secretos; mañana, cuando entre Vertex AI, será la misma identidad la que invoque a los modelos. No hay API keys que rotar: es IAM de punta a punta.
El backend es el único que guarda estado de negocio: habla con la base de datos, valida los JWT, aplica las reglas. Y expone todo bajo el prefijo /api, que es la otra mitad del contrato con el rewrite de Firebase.
La frontera
La regla que resume todo: si se puede servir como archivo, va a Firebase Hosting; si necesita un proceso corriendo, va a Cloud Run. El dominio compartido hace que desde el browser se vea como una sola aplicación, pero cada mitad se despliega, escala y falla por separado. Esa independencia es justo lo que Cloud Build va a explotar en el pipeline: construir las dos mitades en paralelo y desplegarlas en el orden correcto.


