ladder-api
ladder-api
Section titled “ladder-api”Backend LADDER Dev. Hono + Node + Drizzle ORM + PostgreSQL + Better Auth,
arquitectura hexagonal. API pa ladder-web (Astro) — reemplaza Supabase
completo pa este producto.
Contexto completo: CLAUDE.md, .claude/ARCHITECTURE.md, .claude/STACK.md,
docs/adr/.
| Tecnología | Versión | Pa qué |
|---|---|---|
| Hono | ^4.6 | Router HTTP liviano |
| @hono/node-server | ^1.14 | Adapter Node |
| @hono/zod-validator | ^0.4 | Valida DTOs vía Zod en middleware |
| drizzle-orm | ^0.45 | ORM, cliente tipado desde schema.ts sin generación |
| drizzle-kit | ^0.31 | CLI, genera/aplica migraciones |
| Better Auth | ^1.1 | Sesiones + email/password, drizzleAdapter |
| PostgreSQL | 15+ | Datasource |
| TypeScript | ^5.7 | Estricto, sin any |
| Vitest | ^2.1 | Tests |
Hono no NestJS: API tamaño moderado, sin DI/decoradores, mismo type-safety
con Zod, menos ceremonia. Drizzle no Prisma: 100% TS, sin binario nativo,
tipado directo de schema.ts, corre en edge. Detalle → .claude/STACK.md,
docs/adr/001-initial-architecture.md.
Arquitectura
Section titled “Arquitectura”Hexagonal — domain/ y application/ cero dep de infrastructure/:
ladder-web (Astro) --HTTP--> ladder-api (Hono) ├── domain/ entidades + puertos ├── application/ casos de uso └── infrastructure/ Hono routes, Drizzle, Better Auth └── PostgreSQLdomain/— entidades + puertos (interfaces repo). Cero dep externa.application/— casos de uso, orquesta dominio. No sabe de HTTP ni Drizzle.infrastructure/— único lugar que toca mundo exterior:persistence/drizzle/—client.ts+schema.ts(fuente de verdad) +*.drizzle-repository.ts.auth/—better-auth.config.ts,drizzleAdapter.http/routes/— routers Hono.http/middleware/—auth.middleware.ts,permissions.middleware.ts.http/schemas/— DTOs Zod.
main.ts— composition root: monta/api/auth/*+ rutas de dominio.
Integraciones externas: solo PostgreSQL (Drizzle) + Better Auth. Nada más
— analytics/leads son dominio propio. Detalle → .claude/ARCHITECTURE.md,
docs/adr/001-initial-architecture.md.
Estructura del repo
Section titled “Estructura del repo”drizzle/migrations/ historial migraciones, no tocar a mano
src/├── domain/ núcleo, cero dep externa├── application/ casos de uso├── infrastructure/│ ├── persistence/drizzle/ client.ts + schema.ts + *.drizzle-repository.ts│ ├── auth/ better-auth.config.ts│ └── http/│ ├── routes/ Hono routers│ ├── middleware/ auth + permissions│ └── schemas/ Zod DTOs└── main.ts composition root6 dominios reales: lead, form-submission, analytics, newsletter,
service-price, contact-form. Detalle → CURRENT_STATUS.md.
Quick start
Section titled “Quick start”npm installcp .env.example .env # rellenar DATABASE_URL, BETTER_AUTH_SECRETnpx @better-auth/cli generatenpm run db:migratenpm run devEnv vars
Section titled “Env vars”PORT=3000DATABASE_URLBETTER_AUTH_SECRET # openssl rand -hex 32BETTER_AUTH_URL # http://localhost:3000 devPropias del mundo: CORS_ORIGIN (CORS + trustedOrigins), GLITCHTIP_DSN
(errores, src/instrument.ts), R2_* (imágenes de sitio/producto, ver
.claude/STACK.md), RESEND_API_KEY / RESEND_FROM_EMAIL (default
forms@ladderdev.com) / TURNSTILE_SECRET_KEY (POST /forms/contact, ver
docs/contact-form-integration.md).
Scripts
Section titled “Scripts”| Script | Comando | Qué hace |
|---|---|---|
db:migrate |
drizzle-kit generate && migrate |
genera+aplica (dev) |
db:deploy |
drizzle-kit migrate |
solo aplica (prod, Coolify) |
db:studio |
drizzle-kit studio |
explorador visual :4983 |
db:seed |
tsx scripts/seed.ts |
datos semilla |
db:promote-admin |
tsx scripts/promote-admin.ts |
usuario → role: admin |
Decisiones clave
Section titled “Decisiones clave”schema.tsfuente de verdad — Drizzle no genera cliente, tipado sale de ahí.- Migraciones dev/prod separadas — nunca
drizzle-kit pushen prod, salta journal. - Better Auth con
drizzleAdapter(db, { provider: 'pg', schema })— tablas User/Session/Account/Verification viven enschema.ts.
Promover admin
Section titled “Promover admin”Signup normal + npm run db:promote-admin -- <email> + relogin (JWT necesita
refrescar role).
Formulario de contacto (landings de clientes)
Section titled “Formulario de contacto (landings de clientes)”Cómo dar de alta un sitio nuevo, configurar dominios permitidos y embeber el
widget → docs/contact-form-integration.md. Decisión de fondo →
docs/adr/004-contact-form-endpoint.md.
Contrato de API
Section titled “Contrato de API”_shared/contracts/ladder/api-contract.yaml (symlink) — fuente real
dist/api-contract.yaml, generado con npm run contract:generate. Regenerar
tras cambio de rutas/schemas en infrastructure/http/.
Conectar a producción (Coolify)
Section titled “Conectar a producción (Coolify)”Host de DATABASE_URL en .env prod es nombre interno de la red Docker
Coolify — no resuelve fuera del VPS. Pa conectar local (migraciones, seed,
db:studio), abrí túnel SSH primero:
ssh -L 5433:127.0.0.1:5433 "${LADDER_TUNNEL_USER:-root}@100.105.232.32" -NPuerto local 5433 (quedó bloqueado a nivel Windows/WinNAT-Hyper-V un rato
el 2026-08-29, se usó 5555 de workaround temporal — ya liberado, vuelto a
5433). .claude/hooks/ensure-tunnel.sh lo levanta solo si hace falta. Con
túnel abierto, scripts db:* usan DATABASE_URL de .env.local
(localhost:5433), apunta a prod vía túnel.