Skip to content

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.

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
└── PostgreSQL
  • domain/ — 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.

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 root

6 dominios reales: lead, form-submission, analytics, newsletter, service-price, contact-form. Detalle → CURRENT_STATUS.md.

Terminal window
npm install
cp .env.example .env # rellenar DATABASE_URL, BETTER_AUTH_SECRET
npx @better-auth/cli generate
npm run db:migrate
npm run dev
Terminal window
PORT=3000
DATABASE_URL
BETTER_AUTH_SECRET # openssl rand -hex 32
BETTER_AUTH_URL # http://localhost:3000 dev

Propias 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).

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
  • schema.ts fuente de verdad — Drizzle no genera cliente, tipado sale de ahí.
  • Migraciones dev/prod separadas — nunca drizzle-kit push en prod, salta journal.
  • Better Auth con drizzleAdapter(db, { provider: 'pg', schema }) — tablas User/Session/Account/Verification viven en schema.ts.

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.

_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/.

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:

Terminal window
ssh -L 5433:127.0.0.1:5433 "${LADDER_TUNNEL_USER:-root}@100.105.232.32" -N

Puerto 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.

Ver diagrama interactivo