# AI Tender Copilot Saudi

A bilingual (Arabic RTL + English LTR) SaaS platform that helps Saudi companies analyze government tenders, track compliance, draft proposals with AI, and collaborate in real time.

## Run & Operate

- `pnpm --filter @workspace/api-server run dev` — build + start API server (port 8080)
- `pnpm --filter @workspace/tender-copilot run dev` — start Vite frontend dev server
- `pnpm run typecheck` — full typecheck across all packages
- `pnpm --filter @workspace/api-spec run codegen` — regenerate API hooks and Zod schemas from OpenAPI spec
- `pnpm --filter @workspace/db run push` — push DB schema changes (dev only; never in production)

### Required environment variables

| Variable | Purpose |
|---|---|
| `DATABASE_URL` | PostgreSQL connection string |
| `CLERK_PUBLISHABLE_KEY` | Clerk frontend publishable key |
| `CLERK_SECRET_KEY` | Clerk backend secret key |
| `OPENAI_API_KEY` | GPT-4o for tender analysis + proposal generation |
| `STRIPE_SECRET_KEY` | Stripe subscription billing |
| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signature validation |
| `RESEND_API_KEY` | Transactional email (deadline reminders) |
| `ALLOWED_ORIGINS` | Comma-separated extra CORS origins (optional) |

## Stack

- **Runtime**: Node.js 24, TypeScript 5.9, pnpm workspaces
- **Frontend**: React 19 + Vite, Tailwind CSS + shadcn/ui, i18next (Arabic/English), TanStack Query v5, wouter routing, socket.io-client
- **Backend**: Express 5, Drizzle ORM + PostgreSQL, Zod validation, Orval-generated API client
- **Auth**: Clerk (JWT middleware + `requireAuth` helper; admin role via `users.role`)
- **AI**: OpenAI GPT-4o — tender analysis (bid score, risks, recommendations) and proposal draft generation
- **Real-time**: Socket.IO — authenticated rooms per tender; broadcasts for analysis completion, compliance changes, proposal updates
- **Billing**: Stripe subscriptions (plans: starter/professional/enterprise)
- **Email**: Resend — deadline reminder cron runs daily at 08:00 AST

## Where things live

```
artifacts/
  api-server/src/
    routes/          — Express route handlers (tenders, analyses, compliance, proposals, ...)
    lib/
      auth.ts        — requireAuth middleware + AuthRequest type
      openai.ts      — GPT-4o analysis + proposal generation
      socket.ts      — Socket.IO init + authenticated room join + broadcast helpers
      cron.ts        — node-cron deadline reminder job
      mailer.ts      — Resend transactional email
  tender-copilot/src/
    pages/           — One file per route (dashboard, tenders, compliance, proposal, ...)
    components/layout/AppLayout.tsx — Sidebar nav (admin link gated by role)
    lib/
      socket-client.ts — useSocketRoom() hook (passes Clerk token in handshake auth)
      i18n.ts         — i18next setup (persists language to localStorage)
packages/
  db/src/schema/     — Drizzle table definitions (source of truth for DB shape)
  api-spec/          — OpenAPI YAML spec (source of truth for API contract)
  api-client-react/  — Orval-generated TanStack Query hooks
  api-zod/           — Orval-generated Zod request/response schemas
```

## Architecture decisions

- **Orval codegen from OpenAPI**: all API hooks and Zod schemas are generated; never hand-written. After changing `api-spec/openapi.yaml`, run `pnpm --filter @workspace/api-spec run codegen`.
- **Clerk JWT in Socket.IO**: frontend calls `getToken()` before socket init and passes it in `socket.auth.token`; backend middleware runs `verifyToken()` + DB user lookup before allowing any room join.
- **Company-scoped data**: every DB table uses `companyId` as the tenancy boundary; route handlers verify ownership before reads/writes.
- **CORS policy**: only `.replit.dev`, `.repl.co`, `.replit.app` origins and explicit `ALLOWED_ORIGINS` env var are allowed — `origin: true` is intentionally avoided.
- **Error middleware**: centralized 4-param Express handler in `app.ts` returns structured JSON for all errors; OpenAI key missing → 503 with explicit message.

## User preferences

- Bilingual UI: all user-facing strings go through i18next with `ar` as the default locale.
- Arabic is RTL; the layout automatically mirrors when `i18n.language === "ar"`.

## Gotchas

- The Stripe webhook route (`/api/stripe/webhook`) must receive the raw body — it is registered before `express.json()`.
- Socket.IO path is `/api/socket.io` (not the default `/socket.io`) to stay within the API prefix.
- `uploads/` directory (multer destination) is git-ignored; create it manually if running fresh.
