尚未收集儲存庫資訊。
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗SaaS B2B de gestión equina para yeguadas PRE y picaderos en Andalucía.
npm install
cp .env.example .env
# Edita .env con tus credenciales
# Crear la base de datos y aplicar migraciones
npm run db:migrate
# Cargar datos demo
npm run db:seed
npm run dev
Abre http://localhost:3000.
El seed crea el tenant yeguada-demo-andalucia con 5 caballos PRE.
Accede a: http://localhost:3000/yeguada-demo-andalucia/caballos
DATABASE_URL (pooling) y DIRECT_URL de Project Settings > Database-- Ejecutar en el SQL Editor de Supabase tras prisma migrate
ALTER TABLE "Horse" ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_horse ON "Horse"
USING ("tenantId"::text = current_setting('app.current_tenant', true));
-- Repetir para todas las tablas con tenantId
Google es la vía de entrada principal: sin GOOGLE_CLIENT_ID y
GOOGLE_CLIENT_SECRET, la pantalla de acceso lo avisa en vez de ofrecer un
botón que no funciona.
http://localhost:3000/api/auth/callback/google
(y el equivalente https://tu-dominio/api/auth/callback/google en producción)openid email profile) no hace falta verificación, pero en
modo prueba solo entran 100 usuarios añadidos a manoGOOGLE_CLIENT_ID y GOOGLE_CLIENT_SECRET al .envResend solo envía desde un dominio verificado: el único que funciona sin
verificar es resend.dev y sirve únicamente para pruebas. Sin EMAIL_FROM con
dominio propio, los envíos se omiten y queda anotado en el log; no se inventa un
remitente que rebotaría.
RESEND_API_KEY y EMAIL_FROM="Relincho <[email protected]>" al .envMientras no haya dominio verificado, el acceso por enlace mágico se puede
mantener apagado con EMAIL_LOGIN_ENABLED (por defecto se apaga solo en
producción si Google está configurado).
Hace falta para las fotos de los caballos y para el gestor documental. El bucket de documentos debe ser privado: los pasaportes, radiografías y analíticas se sirven con URLs firmadas que caducan, nunca desde una URL pública permanente.
R2_* al .env (la web pública opcional solo se usa
para las fotos, no para documentos)Sin R2_*, en local las subidas van a public/uploads (que está ignorado por
git); en producción esa ruta es de solo lectura y el gestor avisa de que el
almacenamiento no está configurado.
Crea cuenta en stripe.com
Crea los precios de Stripe (una vez, en modo real) con estas cifras, sin IVA, y
pon sus ids en el .env. Toda la lógica de precios vive en src/lib/pricing.ts:
si cambia una cifra, se cambia allí y en Stripe.
| Variable | Precio |
|---|---|
STRIPE_PRICE_CUADRA_MONTH / _YEAR | 69 €/mes · 690 €/año |
STRIPE_PRICE_RENDIMIENTO_MONTH / _YEAR | 149 €/mes · 1.490 €/año |
STRIPE_PRICE_YEGUADA_MONTH / _YEAR | 349 €/mes · 3.490 €/año (desde) |
STRIPE_PRICE_ADDON_BILLING_MONTH / _YEAR | 39 €/mes · 390 €/año (Pupilaje y Facturación) |
STRIPE_PRICE_ADDON_HORSES_MONTH / _YEAR | 25 €/mes · 250 €/año por bloque de 10 caballos (cantidad = bloques) |
STRIPE_PRICE_MIGRATION | 149 € pago único |
STRIPE_COUPON_FOUNDER | cupón 40 %, duración forever (precio de fundador) |
STRIPE_PRICE_PRO | heredado: solo si hay yeguadas de la beta que ya pagaban 79 € |
El plan Cuaderno (gratis, 5 caballos) no necesita precio. Las yeguadas de la
beta con plan = "starter" conservan su límite de 15 caballos.
Configura el webhook: stripe listen --forward-to localhost:3000/api/webhooks/stripe
Añade las variables STRIPE_* al .env
# Instala Vercel CLI
npm i -g vercel
# Despliega
vercel
# Configura variables de entorno en vercel.com/[proyecto]/settings/environment-variables
# El build aplica las migraciones pendientes antes de compilar:
# prisma generate && prisma migrate deploy && next build
# Ojo: `migrate deploy` también corre en los despliegues de preview, así que
# ambos apuntan a la base de datos real. Si algún día hace falta una base de
# datos de preview, hay que separar los entornos.
# Los crons de vercel.json se activan solos en producción:
# /api/cron/reminders cada día a las 08:00 UTC
# /api/cron/weekly-digest lunes a las 07:00 UTC
# Vercel programa en UTC: las 08:00 son las 10:00 de España en verano.
# Ambos exigen CRON_SECRET en la cabecera Authorization.
npm run check # tipos + pruebas unitarias
npm run check:http # contra un servidor levantado (por defecto localhost:3000)
npm run check:http -- https://relincho.vercel.app # contra la instancia real
check:http comprueba lo que de verdad rompe la venta si falla: que el panel
demo abra sin cuenta, que en la demo no se pueda escribir, que sin cabecera
de yeguada no se lea nada, que un panel ajeno siga pidiendo sesión, que los
crons exijan su secreto y que la pantalla de acceso ofrezca una vía que pueda
funcionar.
npm run lint existe, pero arrastra errores anteriores a este trabajo (en su
mayoría no-explicit-any): no lo uses todavía como puerta de calidad.
El QR del flyer no puede caer en un muro de login. /demo ofrece "Probar sin
registro" y esa entrada abre la yeguada de demostración dentro del panel real:
DEMO_TENANT_SLUG (yeguada-demo-andalucia por defecto).src/server/trpc/init.ts y corta cualquier mutation de cualquier router
con un 403, así que no hay que proteger procedure a procedure.src/components/layout/demo-banner.tsx.Los datos salen del seed: npm run db:seed (yeguada, caballos y sanidad) y
npm run seed:rendimiento (periodización, que es lo que llena rendimiento).
/[yeguada]/documentos guarda pasaportes, radiografías, analíticas, contratos
y seguros, cada uno ligado a su caballo (el portal del propietario los ve en solo
lectura). Los detalles que importan:
ownsDocumentKey comprueba que la clave esté dentro de la carpeta de esa
yeguada, para que nadie adjunte a su ficha el fichero de otra.DOCUMENTS_QUOTA_MB (512 MB por defecto) y se
comprueba en el servidor, no en el navegador.reportError (src/lib/observability.ts). Si defines ERROR_WEBHOOK_URL con
un webhook de Slack o Discord, también llega el aviso allí. No hay servicio
externo de monitorización todavía: cambiar a Sentry es sustituir ese fichero./api/report-error, que los reenvía
al mismo canal con un tope por IP.?src=flyer-jerez se lleva esa marca y el alta la escribe en
Tenant.acquisitionSource. Se apunta el primer toque, así que el QR de una
feria sigue contando semanas después. Para leerlo: npm run db:studio.npm run check # tsc --noEmit + pruebas unitarias
npm run check:quality # puerta local: tipos + pruebas + accesibilidad + diff
npm run check:a11y # contratos automatizados de accesibilidad
npm run check:http # comprobaciones HTTP contra un servidor levantado
npm run verify:engines # invariantes del motor de rendimiento
npm run db:generate # Genera el cliente Prisma
npm run db:migrate # Crea y aplica migraciones
npm run db:push # Push directo a la BD (dev)
npm run db:studio # Abre Prisma Studio
npm run db:seed # Carga datos demo
La definición de producto, las personas objetivo, los principios y las prioridades
están en docs/product-definition.md. Es una hipótesis
de producto que debe validarse con pruebas reales; no representa feedback atribuido
a una persona concreta.
La checklist de regresión visual, responsive y accesibilidad está en
docs/visual-regression-checklist.md. Debe
usarse junto con las comprobaciones automáticas antes de publicar cambios de
interfaz. En GitHub, el workflow Quality gate ejecuta automáticamente
npm run check:quality en cada pull request y en cada push a master.
src/
├─ app/
│ ├─ (auth)/ # Login, magic link
│ ├─ (app)/
│ │ ├─ [tenantSlug]/ # App multi-tenant
│ │ └─ onboarding/ # Registro de nueva finca
│ └─ api/
│ ├─ trpc/ # Handler tRPC
│ ├─ webhooks/ # Stripe webhook
│ └─ cron/ # Recordatorios email
├─ server/
│ ├─ db/ # Prisma + helper RLS
│ ├─ auth/ # Auth.js config
│ ├─ trpc/ # Routers tRPC
│ ├─ actions/ # Server Actions
│ └─ services/ # R2, email, billing
├─ components/ # shadcn/ui + componentes
└─ lib/ # Utils, formatters, stripe
prisma/
├─ schema.prisma # Modelo de datos completo
└─ seed.ts # Datos demo
no-explicit-any en 20 ficheros)