152 lines
5.6 KiB
Markdown
152 lines
5.6 KiB
Markdown
# AGENTS.md
|
|
|
|
## Commands
|
|
|
|
```sh
|
|
# 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
|
|
|
|
### Cookie
|
|
|
|
- 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.
|