Files
playzer/AGENTS.md

9.4 KiB

AGENTS.md

Commands

# Root (runs both frontend + backend)
bun run dev

# Individual packages
bun run dev:frontend
bun run dev:backend
bun run build:frontend

# Linting (Biome - ESLint was replaced)
bun run lint
bun run lint:fix  # fixes and formats

# Prisma (run from backend)
bun --filter backend prisma:generate  # regenerate client after schema changes
bun --filter backend prisma:migrate    # apply migrations

Architecture

apps/frontend/      # React + Vite + TanStack Router/Query
apps/backend/       # Bun + Hono + Prisma
packages/api-contract/  # Shared Zod schemas, types, route definitions

Shared contract pattern:

  1. Update packages/api-contract first (schemas/types)
  2. Implement handler in apps/backend
  3. Consume from apps/frontend via workspace import @repo/api-contract

Authentication (Better Auth)

The project uses Better Auth for session management, replacing the legacy Supabase Auth.

  • Backend Context: AppContext (mapped via AppEnv) provides c.get('user') and c.get('session').
  • User Roles: The User model includes a role field (default: member). Use requireSuperAdmin middleware for protected admin routes.
  • Session Persistence:
    • Backend: Hono CORS must have credentials: true.
    • Frontend: authClient must have fetchOptions.credentials: 'include'.
    • Frontend: Axios instance must have withCredentials: true.

Key Quirks

  • Prisma 7: Uses prisma.config.ts, requires DATABASE_URL env var at generate time.
  • Gravatar Fallback: Users without an image get an automatic Gravatar Identicon.
    • Backend: Handled in user.service.ts for profile API.
    • Frontend: Handled in AuthProvider (auth.tsx) for the session state.
  • Zod v4: Use .issues instead of .errors for validation errors.
  • TanStack Router: Routes are code-generated. Use --filter frontend build not raw vite build.

Real-time Updates (SSE)

The project uses Server-Sent Events (SSE) for real-time booking updates in the admin panel.

  • Backend: apps/backend/src/lib/sse.ts provides the sseManager and SSE endpoint /api/events/:channel.
  • Event flow: When a public booking is created, the handler emits to channel complex:{complexId}.
  • Frontend: home-page.tsx connects to the SSE endpoint and invalidates the bookings query on new events.

Important - Multi-process limitation: The current SSE implementation works with a single Bun process only. If you deploy with multiple workers (e.g., clustering in Dokploy), events won't be shared across workers. For production with multiple workers, use Redis for pub/sub:

  • Install @hono/node-server or a Redis client
  • Replace sseManager with Redis pub/sub
  • Or fall back to Socket.io with Redis adapter

Docker Deployment (Dokploy)

  • Build Context: ./ (monorepo root)
  • Dockerfile: Dockerfile (in root, not apps/backend/)
  • Required args: DATABASE_URL, VITE_API_BASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL
  • bun.lock must be tracked: It's in .gitignore by default - remove it for Docker builds

Linting

Biome is used (not ESLint). Config in biome.json:

  • Line width: 100
  • Quote style: single
  • Semicolons: always
  • noUnusedVariables: warn

Environment Variables

Backend (apps/backend/.env):

  • DATABASE_URL - PostgreSQL connection (required for Prisma)
  • BETTER_AUTH_SECRET, BETTER_AUTH_URL - Auth core
  • APP_BASE_URL - Frontend URL for trusted origins
  • CORS_ORIGIN - Frontend URL for CORS policy
  • GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET - Google OAuth (see below)

Frontend (apps/frontend/.env):

  • VITE_* prefix required (Vite embeds these at build time)
  • VITE_API_BASE_URL - Backend URL (default: http://localhost:3000)

Google OAuth

To enable login with Google:

  1. Create OAuth credentials in Google Cloud Console:

    • Application type: Web application
    • Authorized redirect URI: {BETTER_AUTH_URL}/api/auth/callback/google
  2. Add env vars to .env.example:

    GOOGLE_CLIENT_ID=your-google-client-id
    GOOGLE_CLIENT_SECRET=your-google-client-secret
    
  3. The backend configures socialProviders.google automatically.

  4. Frontend uses authClient.signIn.social({ provider: 'google', callbackURL: 'http://localhost:5173' }).

Complex Selection (Multi-tenancy)

The app supports users with multiple complexes. Each user has a role per complex (ADMIN or EMPLOYEE), stored in ComplexUser table.

Flow

  1. Login (password or Google OAuth):

    • After auth, call GET /api/complexes/mine to get user's complexes
    • If 1 complex → auto-select via POST /api/complexes/select → redirect to /
    • If >1 complexes → redirect to /select-complex
  2. Select Complex Page (/select-complex):

    • List all complexes with roles
    • User selects one → POST /api/complexes/select → set cookie → redirect to /
  3. Home Page (/):

    • If no cookie (first visit) → auto-select if 1 complex, else redirect
    • Use GET /api/complexes/me to get current selected complex
  • Name: selected-complex-id
  • httpOnly, secure (prod), sameSite: strict
  • MaxAge: 30 days
  • Set by POST /api/complexes/select

Endpoints

Method Endpoint Description
GET /api/complexes/mine List all complexes with role
GET /api/complexes/me Get currently selected complex
POST /api/complexes/select Select complex (sets cookie)

Profile Role

The profile page shows the user's role for the selected complex, not the global user role. This is fetched from ComplexUser table using the cookie value.

Frontend API Client

The API client is split into modular files for better maintainability and testability.

Structure

lib/
├── api-client.ts    # Main barrel file (~70 lines)
└── api/
    ├── base.ts        # ApiClientError, configureApiClient, apiBaseUrl
    ├── http.ts        # Axios instance with interceptors
    ├── index.ts       # Re-exports all resources
    └── resources/
        ├── complexes.ts   # Complex endpoints
        ├── courts.ts      # Court endpoints
        ├── bookings.ts    # Public & admin booking endpoints
        ├── user.ts        # User profile endpoint
        ├── sports.ts      # Sport endpoints
        ├── onboarding.ts  # Onboarding endpoints
        └── plans.ts       # Plan endpoints

Using the API Client

import { apiClient, authClient, ApiClientError, configureApiClient } from '@/lib/api-client';

// Via apiClient (barrel)
await apiClient.complexes.listMine();
await apiClient.publicBookings.getAvailability('my-club', { date: '2026-04-20' });

// Direct imports (for better tree-shaking)
import { complexes, getAvailability } from '@/lib/api';
await complexes.listMine();
await getAvailability('my-club', { date: '2026-04-20' });

Adding a New Resource

  1. Create lib/api/resources/[resource].ts
  2. Export functions using http from ../http
  3. Add exports in lib/api/index.ts

Email Templates

Todos los emails deben usar la misma estética. El layout compartido está en apps/backend/src/emails/booking-confirmation.ts.

Layout (wrapLayout)

Exportado como wrapLayout(content). Proporciona:

  • Fondo: #edf7f4
  • Card blanca: max-width: 520px, border-radius: 28px, box-shadow: 0 24px 70px rgba(15,23,42,0.12), border: 1px solid rgba(5,9,20,0.1)
  • Playzer favicon: {APP_BASE_URL}/playzer-favicon-512-transparent.png (está en apps/frontend/public/)
import { wrapLayout } from '@/emails/booking-confirmation';

const html = wrapLayout(`<tr>...contenido...</tr>`);

Estructura de cada email

Sección Descripción
Header Dos columnas: badge pill a la izquierda + Playzer (favicon + texto) a la derecha. El badge usa border-radius: 999px, padding: 4px 12px, font-size: 11px, font-weight: 700, letter-spacing: 0.14em, text-transform: uppercase.
Card fecha/hora Fondo #f0fdf4, borde 1px solid rgba(5,150,105,0.3), border-radius: 24px. Siempre verde aunque el email sea de cancelación.
Grilla detalles border: 1px solid #e5e7eb, border-radius: 22px, dos celdas de 50% con border-right en la primera.
Botón CTA Tabla con fondo #059669, border-radius: 12px, link blanco con padding: 14px 32px.
Pie Sin pie de marca. Solo texto secundario opcional centrado si es necesario.
Sin botones de acción Los emails de booking no incluyen los botones "Compartir por WhatsApp" ni "Hacer otra reserva".

Colores de badges según estado

Estado Fondo badge Texto badge
Confirmado #f0fdf4 #15803d
Cancelado #fef2f2 #dc2626
No concretado #fffbeb #d97706
Neutro (verificación, etc.) #f4f4f5 #71717a

Archivos de templates

Archivo Templates
apps/backend/src/emails/booking-confirmation.ts bookingConfirmationHtml, bookingCancelledHtml, bookingNoShowHtml + exporta wrapLayout
apps/backend/src/lib/auth.ts Email de verificación de Better Auth (usa wrapLayout inline)

Regla general

Para emails nuevos: importar wrapLayout, construir el HTML interno con <tr>s, usar la misma estructura de cabecera (badge + Playzer), y nunca incluir el pie "Playzer — Reserva de canchas online". Usar APP_BASE_URL para construir URLs absolutas al logo.