Deployment
PinTeach runs on 4 services across 3 providers. All infrastructure is in EU regions for GDPR compliance.
Architecture Overview
Section titled “Architecture Overview”pinteach.com ─── Cloudflare Worker `pinteach-web` (repo aparte)pinteach.app ─── Cloudflare Worker (React SPA)api.pinteach.app ─── Cloudflare Worker (Hono), detrás del proxy stranglerdocs.pinteach.app ─── Cloudflare Pages (Starlight docs, static)
Base ─── Cloudflare D1 (`pinteach-d1`, WEUR)DNS + CDN ─── CloudflareInfrastructure
Section titled “Infrastructure”Cloudflare D1
Section titled “Cloudflare D1”| Setting | Value |
|---|---|
| Producción | pinteach-d1 (dd50c4c0-92dd-409c-917d-f37208716989), WEUR |
| Desarrollo | pinteach-d1-dev (9d54af21-acc8-429a-bf04-a9b3957c9e74) |
| Migraciones | packages/db/src/migrations, las aplica deploy-api.yml |
| Local | libsql sobre file:.dev/pinteach.db |
Fue Neon Postgres por Hyperdrive hasta el corte a D1 del 7-sep-2026. Neon y la
configuración de Hyperdrive se retiraron el 1-oct-2026; el export previo está en
el bucket R2 privado pinteach-neon-export (ver apps/api/docs/CUTOVER-D1.md).
Services
Section titled “Services”API — Cloudflare Worker (api.pinteach.app)
Section titled “API — Cloudflare Worker (api.pinteach.app)”El servicio de Cloud Run se borró el 7-sep-2026 y con él el Dockerfile, el
registro de imágenes y el min-instances=1 que hacía falta para que BullMQ no
se durmiera. Hoy la API es un Worker.
| Ajuste | Valor |
|---|---|
| Worker | pinteach-api (apps/api/wrangler.jsonc, bloque env.production) |
| Entrada | src/worker/index.ts (Hono 4) |
| Base | D1 por el binding DB |
| Otros bindings | KV, CLASSROOM (Durable Object), CLASSROOM_ASSETS (R2), GOOGLE_QUEUE (Queues + DLQ) |
| Crons | triggers.crons — ver Background Jobs |
| Dominio | api.pinteach.app, propiedad del proxy deploy/api-proxy |
El proxy strangler
Section titled “El proxy strangler”api.pinteach.app no apunta al Worker directamente: el dueño del dominio
es deploy/api-proxy, que enruta por prefijo. Su lista MIGRATED decide qué
va al Worker por service binding; lo demás salía al origen de Cloud Run, que ya
no existe. Es decir: un prefijo que el Worker sirve y MIGRATED no nombra da
404 en producción, sin error de despliegue ni log raro. Pasó con
/api/classroom y con /_meta.
Por eso el cruce está gateado: apps/api/src/worker/__tests__/proxy-migrated.test.ts
lee los dos ficheros y falla en las dos direcciones. Es la puerta del workflow
deploy-proxy.yml, que además compila el proxy con --dry-run antes de
desplegar.
Despliegue
Section titled “Despliegue”Automático, por GitHub Actions:
| Workflow | Qué despliega | Disparo |
|---|---|---|
deploy-api.yml |
Worker pinteach-api |
push a main que toque apps/api/** o packages/** |
deploy-proxy.yml |
Worker del proxy | push a main que toque deploy/api-proxy/**, con el cruce de rutas como puerta |
deploy-app.yml |
SPA (pinteach.app) |
push a main que toque apps/app/** |
A mano, desde apps/api/:
CI=true CF_API_TOKEN="" CLOUDFLARE_API_TOKEN="" pnpm exec wrangler deploy --env productionLos secretos del Worker se declaran en apps/api/scripts/secretos.ts y se
cruzan con lo desplegado usando pnpm --filter @pinteach/api sync-secrets. El
origen es 1Password (Pinteach/pinteach-api-prod); nunca se teclean a mano.
Variables del Worker
Section titled “Variables del Worker”Las de vars están en wrangler.jsonc (env.production); las secretas, en
scripts/secretos.ts. Ya NO existen PORT, HOST ni REDIS_URL — eran del
proceso Node.
pinteach.com — fuera de este repo
Section titled “pinteach.com — fuera de este repo”pinteach.com lo sirve el Worker pinteach-web (repo enlolab/pinteach-web,
código de jefe-sites) desde el 18-sep-2026. El sitio Astro de este monorepo
(apps/web) se retiró el 1-oct-2026: los perfiles y el directorio viven en la
SPA (/t, /t/$slug), ver la sección siguiente.
React SPA — Cloudflare Worker (pinteach.app)
Section titled “React SPA — Cloudflare Worker (pinteach.app)”| Setting | Value |
|---|---|
| Provider | Cloudflare Workers (Static Assets) |
| Framework | React 19 + Vite 6 (static SPA) |
| Package | @pinteach/app |
| Domain | pinteach.app |
The app is deployed as a Cloudflare Worker serving static assets via wrangler.toml. This gives better analytics, edge middleware support, and a clean .app domain separation from the public marketing site.
wrangler.toml
Section titled “wrangler.toml”name = "pinteach-app"compatibility_date = "2025-05-01"compatibility_flags = ["nodejs_compat"]
[assets]directory = "./dist"html_handling = "auto-trailing-slash"not_found_handling = "single-page-application"not_found_handling = "single-page-application" ensures TanStack Router receives all paths and handles client-side routing correctly.
Deploy
Section titled “Deploy”Automático por deploy-app.yml en cada push a main que toque apps/app/**.
A mano:
pnpm --filter @pinteach/app deploy # vite build && wrangler deployEnvironment variables (Vite build-time)
Section titled “Environment variables (Vite build-time)”| Variable | Value |
|---|---|
VITE_API_URL |
https://api.pinteach.app/api |
Platform Docs — Cloudflare Pages (docs.pinteach.app)
Section titled “Platform Docs — Cloudflare Pages (docs.pinteach.app)”| Setting | Value |
|---|---|
| Provider | Cloudflare Pages |
| Framework | Astro Starlight (static) |
| Package | @pinteach/platform |
| Domain | docs.pinteach.app |
Internal documentation. Not publicly linked from the marketing site.
Build configuration
Section titled “Build configuration”| Field | Value |
|---|---|
| Framework preset | Astro |
| Build command | pnpm install && pnpm build --filter=@pinteach/platform |
| Build output directory | apps/platform/dist |
Environment variables
Section titled “Environment variables”| Variable | Value |
|---|---|
SKIP_DEPENDENCY_INSTALL |
true |
NODE_VERSION |
22 |
DNS Configuration
Section titled “DNS Configuration”pinteach.com (Cloudflare)
Section titled “pinteach.com (Cloudflare)”| Type | Name | Target | Proxy |
|---|---|---|---|
| Worker custom domain | @ y www |
Worker pinteach-web |
(gestionado por Cloudflare) |
pinteach.app (Cloudflare)
Section titled “pinteach.app (Cloudflare)”| Type | Name | Target | Proxy |
|---|---|---|---|
| CNAME | @ / root |
<worker>.workers.dev |
Proxied (orange) |
| CNAME | api |
ghs.googlehosted.com |
DNS only (gray) |
| CNAME | docs |
<pages-project>.pages.dev |
Proxied (orange) |
Cost Estimate (50 teachers/month)
Section titled “Cost Estimate (50 teachers/month)”| Service | Plan | Estimated Cost |
|---|---|---|
| Cloudflare D1 | incluido en Workers de pago | $0 |
| Cloudflare Workers | API + proxy + SPA, plan de pago | incluido |
| Cloudflare Pages | Free (3 projects, unlimited bandwidth) | $0/mo |
| Cloudflare Workers | Free (100K requests/day) | $0/mo |
| Total | ~$16 — $45/mo |
Scaling notes:
- Cloudflare Workers free tier: 100K requests/day — sufficient for a teacher dashboard
Deployment Workflow
Section titled “Deployment Workflow”API y proxy (automático)
Section titled “API y proxy (automático)”deploy-api.yml y deploy-proxy.yml se disparan en cada push a main que
toque sus rutas. Arriba está el detalle y el comando para desplegar a mano.
Static frontends (automatic)
Section titled “Static frontends (automatic)”Cloudflare Pages auto-deploys on push to main:
docs.pinteach.app— rebuilds@pinteach/platform
Database migrations
Section titled “Database migrations”Las aplica deploy-api.yml en cada despliegue
(wrangler d1 migrations apply DB --env production --remote). A mano, desde
apps/api/:
pnpm exec wrangler d1 migrations apply DB --env production --remote