For the current architecture, runtime behavior, AI/provider routing, RAG design, upload pipeline, and operations runbook, use:
docs/ENGINEERING_HANDOFF_GUIDE.md
This README is kept for historical context and may not reflect all recent implementation changes.
Version: Current (February 2026)
Stack: React + TypeScript (frontend) · Express.js + TypeScript (backend) · PostgreSQL + Drizzle ORM · Google Gemini AI · Kimi K2.5 (Apex)
- Project Overview
- Repository Structure
- Technology Stack
- Environment Variables
- Database Schema
- Authentication System
- User Tier & Usage Limit System
- AI Integration Architecture
- Frontend Architecture
- Backend Routes Reference
- Feature-by-Feature Flow
- Registration & Login
- Main AI Chat (Al Wakeelo Engine)
- Judgment Search
- Statute Search & Statute View
- Legal Drafting
- Contract Drafting
- File Attachments & Audio Transcription
- Apex AI Models (Kimi K2.5)
- Conversation Sharing
- Bookmarks
- Knowledge Vault (User Documents)
- Search History
- Organization & Team Collaboration
- Admin Panel
- Password Reset
- Knowledge Context System
- Caching System
- Cost Tracking & Analytics
- PWA & Install Guide
- Startup Processes
- Build & Deployment
Al Wakeelo is an AI-powered Pakistani legal assistant web application. It provides:
- A chat interface where users consult an AI legal advisor (Google Gemini / Kimi K2.5)
- Pakistani judgment search and case law lookup
- Pakistani statute search with table-of-contents navigation
- AI-powered legal document and contract drafting
- File upload and audio transcription for case context
- A personal knowledge vault for user documents
- Team collaboration through organizations (Enterprise tier)
- An admin panel for content management and system analytics
- Subscription tiers: Free (10 queries/month), Pro (500/month), Enterprise (unlimited)
The system is designed so the AI always works within Pakistani legal context, using a layered knowledge system: internal statutes/case law database → GitHub-synced legal library → admin-uploaded documents → user's own case files → organization shared knowledge.
Alwakeelo/
├── client/ # React SPA (frontend)
│ ├── public/
│ │ ├── manifest.json # PWA manifest
│ │ └── sw.js # Service worker (PWA)
│ └── src/
│ ├── App.tsx # Root router & auth guard
│ ├── main.tsx # React entry point
│ ├── components/
│ │ ├── app-shell.tsx # Sidebar layout wrapper for protected pages
│ │ ├── legal-markdown.tsx # Markdown renderer with clickable statute/case links
│ │ ├── reference-cards.tsx# Parses ```references JSON blocks from AI output
│ │ └── ui/ # shadcn/ui component library
│ ├── hooks/
│ │ └── use-auth.ts # useAuth() — current user + logout
│ ├── lib/
│ │ └── queryClient.ts # TanStack Query client + apiRequest helper
│ └── pages/
│ ├── landing.tsx
│ ├── auth.tsx
│ ├── dashboard.tsx
│ ├── chat.tsx # Reusable ChatModule used by 3 pages
│ ├── judgment-search.tsx
│ ├── judgment-view.tsx
│ ├── statute-search.tsx
│ ├── statute-view.tsx
│ ├── legal-drafting.tsx
│ ├── contract-drafting.tsx
│ ├── case-documents.tsx
│ ├── bookmarks.tsx
│ ├── history.tsx
│ ├── knowledge-vault.tsx
│ ├── organization.tsx
│ ├── admin-panel.tsx
│ ├── user-panel.tsx
│ ├── admin-setup.tsx
│ ├── shared-conversation.tsx
│ ├── install-app.tsx
│ ├── privacy.tsx
│ ├── terms.tsx
│ ├── forgot-password.tsx
│ └── reset-password.tsx
├── server/
│ ├── index.ts # Express app entry point
│ ├── routes.ts # All API route handlers (~2500 lines)
│ ├── storage.ts # Database abstraction layer (Drizzle queries)
│ ├── apex-ai.ts # Kimi K2.5 (Moonshot API) integration
│ ├── github-sync.ts # Syncs legal texts from GitHub on startup
│ ├── auto-extract-caselaw.ts # Background AI extraction of case law
│ ├── vite.ts # Vite dev middleware integration
│ └── replit_integrations/
│ └── auth/
│ └── routes.ts # Auth routes (register, login, OAuth, password reset)
├── shared/
│ ├── schema.ts # Drizzle ORM schema (all tables)
│ └── models/
│ └── auth.ts # Auth-related table definitions
├── script/
│ └── build.ts # esbuild + Vite production build
├── migrations/ # Drizzle migration output
├── database_export.sql # Last exported database dump
└── DEVELOPER_GUIDE.md # This file
| Layer | Technology |
|---|---|
| Frontend framework | React 18 + TypeScript |
| Frontend bundler | Vite |
| Client routing | Wouter (NOT React Router) |
| Server state | TanStack React Query |
| UI components | shadcn/ui (Radix UI + Tailwind CSS) |
| Animations | Framer Motion |
| Markdown rendering | react-markdown + remark-gfm |
| Backend framework | Express.js + TypeScript |
| Backend runtime | tsx (development), Node.js (production) |
| Database | PostgreSQL |
| ORM | Drizzle ORM + drizzle-kit |
| Sessions | express-session + connect-pg-simple |
| Auth | bcryptjs (password hashing), Passport.js (Google OAuth) |
| Primary AI | Google Gemini via @google/genai SDK |
| Secondary AI | Kimi K2.5 via Moonshot API (OpenAI-compatible) |
| File processing | multer (upload), unpdf (PDF text), mammoth (DOCX text) |
| Resend API (password reset emails) |
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL |
Yes | PostgreSQL connection string |
GOOGLE_API_KEY |
Yes | Google Gemini AI API key |
SESSION_SECRET |
Yes | express-session signing secret |
GOOGLE_CLIENT_ID |
Optional | Google OAuth client ID |
GOOGLE_CLIENT_SECRET |
Optional | Google OAuth client secret |
MOONSHOT_API_KEY |
Optional | Kimi K2.5 (Apex models) via Moonshot API |
RESEND_API_KEY |
Optional | Resend email service for password resets |
RESEND_FROM_EMAIL |
Optional | Preferred sender address for Resend emails |
FROM_EMAIL |
Optional | Sender address for reset emails |
All tables are defined in shared/schema.ts and shared/models/auth.ts. Migrations are applied with npm run db:push.
| Column | Type | Notes |
|---|---|---|
| id | varchar (UUID) | Primary key, auto-generated |
| varchar | Unique | |
| firstName | varchar | |
| lastName | varchar | |
| profileImageUrl | varchar | Optional |
| passwordHash | varchar | Nullable (null for OAuth users) |
| authProvider | varchar | "email" or "google" |
| subscriptionTier | varchar | "free", "pro", "enterprise" (default: free) |
| isAdmin | boolean | Admin flag (default: false) |
| createdAt | timestamp | Auto |
| updatedAt | timestamp | Auto |
Managed by connect-pg-simple. Never drop this table — it stores active user sessions.
| Column | Type | Notes |
|---|---|---|
| id | serial | Primary key |
| userId | varchar | FK → users |
| token | varchar | 32-byte hex |
| expiresAt | timestamp | 1 hour after creation |
| used | boolean | Marked true after use |
| Column | Type | Notes |
|---|---|---|
| id | serial | Primary key |
| userId | varchar | FK → users |
| title | varchar | Thread title |
| shareToken | varchar | Nullable; set when user shares conversation |
| createdAt | timestamp |
| Column | Type | Notes |
|---|---|---|
| id | serial | Primary key |
| threadId | integer | FK → threads |
| role | varchar | "user", "assistant", "system" |
| content | text | Message content |
| createdAt | timestamp |
User-uploaded files in the Knowledge Vault.
| Column | Type | Notes |
|---|---|---|
| id | serial | Primary key |
| userId | varchar | FK → users |
| title | varchar | |
| filename | varchar | |
| content | text | Extracted text content |
| summary | text | AI-generated summary (optional) |
| createdAt | timestamp |
| Column | Type | Notes |
|---|---|---|
| id | serial | Primary key |
| userId | varchar | FK → users |
| title | varchar | |
| content | text | Full AI response text |
| type | varchar | "al-wakeelo", "draft", "contract" |
| category | varchar | |
| createdAt | timestamp |
| Column | Type | Notes |
|---|---|---|
| id | serial | Primary key |
| userId | varchar | FK → users |
| type | varchar | "judgment", "statute", "chat", "draft", "contract" |
| query | varchar | First 80 chars of query |
| createdAt | timestamp |
Structured Pakistani statute entries (PPC sections, CrPC, CPC, etc.)
| Column | Type | Notes |
|---|---|---|
| id | serial | Primary key |
| shortTitle | varchar | Statute short name |
| section | varchar | Section number/name |
| description | text | Section description |
| punishment | text | Punishment/penalty (if applicable) |
| keywords | text | Searchable keywords |
Pakistani case law (judgments from PLD, SCMR, YLR, MLD, CLC, etc.)
| Column | Type | Notes |
|---|---|---|
| id | serial | Primary key |
| citation | varchar | e.g., "PLD 2019 SC 412" |
| court | varchar | Court name |
| title | varchar | Case title |
| summary | text | Case summary |
| keywords | text | Search keywords |
| uri | varchar | External link (optional) |
Auto-populated on startup by syncing from the GitHub repository ijlalbintariq/law.
| Column | Type | Notes |
|---|---|---|
| id | serial | Primary key |
| filename | varchar | Source filename |
| title | varchar | Document title |
| content | text | Full text content |
| createdAt | timestamp |
Documents uploaded by admins to the shared knowledge vault.
| Column | Type | Notes |
|---|---|---|
| id | serial | Primary key |
| title | varchar | |
| filename | varchar | |
| content | text | |
| category | varchar | |
| uploadedBy | varchar | FK → users (nullable) |
| createdAt | timestamp |
Full-text statute documents uploaded by admins (acts, ordinances, etc.)
| Column | Type | Notes |
|---|---|---|
| id | serial | Primary key |
| title | varchar | |
| filename | varchar | |
| content | text | Full extracted text |
| category | varchar | |
| createdAt | timestamp |
Judgments bookmarked by individual users.
| Column | Type | Notes |
|---|---|---|
| id | serial | Primary key |
| userId | varchar | FK → users |
| citation | varchar | |
| court | varchar | |
| title | varchar | |
| summary | text | |
| keywords | text | |
| uri | varchar | |
| source | varchar | |
| aiAnalysis | text | AI-generated analysis |
| createdAt | timestamp |
| Column | Type | Notes |
|---|---|---|
| id | serial | Primary key |
| endpoint | varchar | Feature name/endpoint |
| queryHash | varchar | SHA-256 of normalized query |
| response | text | Cached AI response |
| hitCount | integer | Number of cache hits |
| createdAt | timestamp | Used to enforce 7-day TTL |
| Column | Type | Notes |
|---|---|---|
| id | serial | Primary key |
| userId | varchar | FK → users |
| feature | varchar | "chat", "search-judgments", "brief", etc. |
| model | varchar | AI model used |
| inputTokens | integer | Estimated input tokens |
| outputTokens | integer | Estimated output tokens |
| estimatedCost | text | Cost in USD (string decimal) |
| createdAt | timestamp | Used for monthly rollup |
| Column | Type | Notes |
|---|---|---|
| id | serial | Primary key |
| name | varchar | |
| description | text | |
| ownerId | varchar | FK → users |
| createdAt | timestamp |
| Column | Type | Notes |
|---|---|---|
| id | serial | Primary key |
| orgId | integer | FK → organizations |
| userId | varchar | FK → users |
| role | varchar | "owner" or "member" |
| joinedAt | timestamp |
| Column | Type | Notes |
|---|---|---|
| id | serial | Primary key |
| orgId | integer | FK → organizations |
| varchar | Invited user's email | |
| status | varchar | "pending", "accepted", "declined" |
| createdAt | timestamp |
| Column | Type | Notes |
|---|---|---|
| id | serial | Primary key |
| orgId | integer | FK → organizations |
| title | varchar | |
| filename | varchar | |
| content | text | |
| uploadedBy | varchar | FK → users |
| createdAt | timestamp |
File: server/replit_integrations/auth/routes.ts
The app uses session-based authentication. Sessions are stored in PostgreSQL via connect-pg-simple.
- On login/register, the server sets
req.session.userId = user.id - express-session serializes this into a cookie (
connect.sid) sent to the browser - On every protected request, the
isAuthenticatedmiddleware checksreq.session.userId - If no session or session expired → 401 Unauthorized
function isAuthenticated(req, res, next) {
if (req.session?.userId) return next();
return res.status(401).json({ message: "Unauthorized" });
}bcryptjs with 12 salt rounds. Passwords are never stored in plain text.
- Frontend calls
GET /api/auth/google/statusto check if Google OAuth is configured - If configured, shows "Continue with Google" button
- User clicks → Google credential returned as JWT token
- Frontend sends token to
POST /api/auth/google/token - Server verifies token with Google OAuth API, finds or creates user in DB
- Creates session exactly like normal login
useAuth() hook in client/src/hooks/use-auth.ts:
- Queries
GET /api/auth/uservia React Query - Returns
{ user, isLoading, logout } - If 401 returned →
user = null→ App.tsx redirects to/auth logout()callsPOST /api/auth/logoutthen invalidates the query
TIER_LIMITS = {
free: { monthly: 10, label: "Free Plan" },
pro: { monthly: 500, label: "Pro Plan" },
enterprise: { monthly: 999999, label: "Enterprise Plan" }
}Every AI endpoint calls checkUsageLimit(userId, feature, res) before processing:
- Query
usageTrackingtable:COUNT(*) WHERE userId = ? AND createdAt >= start_of_month - Get user's tier from
userstable - Enterprise users bypass limits entirely and also bypass the 2-second rate limit
- If
used >= limit→ returns HTTP 429 with tier info - Per-user rate limit: In-memory Map tracks last request time; rejects if < 2 seconds since last request
After every successful AI call:
logUsageCost(userId, feature, model, inputText, outputText)- Estimates tokens:
Math.ceil(text.length / 4) - Calculates cost:
(tokens / 1000) * rate_per_1k - Cost rates:
- gemini-3-flash-preview: $0.00015/1K input, $0.0006/1K output
- gemini-3-pro-preview: $0.00125/1K input, $0.005/1K output
- Inserts row into
usageTracking
GET /api/usage returns:
{
"tier": "pro",
"tierLabel": "Pro Plan",
"monthlyLimit": 500,
"used": 23,
"remaining": 477,
"percentage": 4.6
}Dashboard shows a progress bar; warnings appear at 80% and 100%.
SDK: @google/genai
Config: GOOGLE_API_KEY environment variable
Initialization: getAI() function (lazy singleton)
Models used:
gemini-3-flash-preview— Default; used for chat, search, summaries, drafting (free/pro/enterprise)gemini-3-pro-preview— Upgraded model; used whenturbo: true(Pro/Enterprise only) or for legal briefs
Automatic fallback: If the pro model returns a 429 (quota exceeded), the server transparently falls back to the flash model and re-tries, without the user knowing.
Max token limits by feature:
MAX_OUTPUT_TOKENS = {
chat: 4096,
"search-judgments": 2048,
"search-statutes": 2048,
summarize: 3072,
brief: 6144,
draft: 6144,
toc: 8192,
}File: server/apex-ai.ts
Provider: Moonshot AI API at https://api.moonshot.ai/v1 (OpenAI-compatible)
Config: MOONSHOT_API_KEY environment variable
Model mapping:
| Branded Name | API Model | Tier | Temperature | Thinking | Max Tokens |
|---|---|---|---|---|---|
| Apex | kimi-k2.5 | Pro | 0.6 | Off | 4096 |
| Apex Pro | kimi-k2.5 | Enterprise | 1.0 | On | 8192 |
| Apex Agent | kimi-k2.5 | Enterprise | 0.7 | On | 8192 |
When thinking is enabled, the API returns a reasoning_content field with the model's chain-of-thought, which is passed back to the frontend in the response.
The legal system prompt is returned by getLegalSystemPrompt() in server/routes.ts:
You are Al Wakeelo — Your Digital Lawyer, inspired by Saul Goodman.
- Answer in user's language (English or Urdu)
- Always cite Pakistani sources: PLD, SCMR, YLR, MLD, CLC, PCRLJ
- Mandatory response structure with markdown sections:
### Legal Context
### Statutory Framework & Provisions
### Leading Case Law & Precedents
### Practical Legal Strategy
```references
{ "laws": [...], "judgments": [...] }
The system prompt is dynamically injected with:
- Current date
- Knowledge context from all 6 knowledge sources
- Attached document content (if files uploaded)
- Draft mode instruction (for legal drafting pages)
### 8.4 Streaming Responses
When `stream: true` is sent in the request body, the server uses Server-Sent Events (SSE):
Server → Client (SSE format): data: {"text": "The Pakistan"}\n\n data: {"text": " Penal Code"}\n\n data: {"text": " Section 302"}\n\n data: {"done": true, "model": "gemini-3-flash-preview"}\n\n
Frontend reads the stream chunk by chunk, appending to the message content in real time using React state updates.
### 8.5 Reference Cards
The AI is instructed to append a structured JSON block at the end of every response:
{
"laws": [
{ "name": "Pakistan Penal Code", "section": "302", "description": "..." }
],
"judgments": [
{ "citation": "PLD 2019 SC 412", "title": "...", "summary": "..." }
]
}
The frontend component `reference-cards.tsx` parses this block via `parseReferences()`:
1. Uses regex to find the `references` code block
2. Strips it from the displayed message text
3. Renders it as clickable "Relevant Laws" and "Relevant Judgments" cards below the message
4. Cards navigate to `/statute-search?q=...` and `/judgment-search?q=...`
---
## 9. Frontend Architecture
### Routing (Wouter)
All routing happens in `client/src/App.tsx`. The router checks:
1. If route is public (landing, auth, forgot-password, reset-password, shared conversation, privacy, terms, install) → render directly
2. If user is not authenticated → redirect to `/auth`
3. Otherwise → wrap in `AppShell` and render the matching protected page
### AppShell
`client/src/components/app-shell.tsx` renders the sidebar navigation and the main content area for all protected pages. The sidebar contains links to all features and adapts based on screen size (collapsible on mobile).
### ChatModule (Shared Component)
Three pages reuse the same `ChatModule` component from `chat.tsx`:
- `/al-wakeelo` — type `"chat"`, title "Al Wakeelo Engine"
- `/legal-drafting` — type `"draft"`, title "Legal Drafting"
- `/contract-drafting` — type `"contract-drafting"`, used inside split layout
The `type` field changes the system prompt behavior on the backend.
### LegalMarkdown Component
`client/src/components/legal-markdown.tsx` renders AI responses with:
- Standard markdown formatting (bold, italic, headers, lists, code)
- **Statute links**: `**[Statute Name, Year]**` becomes a link to `/statute-search?q=...`
- **Case citation links**: Patterns like `PLD 2019 SC 412` become links to `/judgment-search?q=...`
### State Management
- **React Query**: All server data (no Redux, no Zustand)
- **Local React state**: UI state (input values, modals, loading flags)
- **chatStateStore**: A module-level object that caches chat messages between page navigations (so returning to a chat page doesn't reset the conversation)
---
## 10. Backend Routes Reference
### Auth Routes (`server/replit_integrations/auth/routes.ts`)
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/auth/register | No | Register with email/password |
| POST | /api/auth/login | No | Login with email/password |
| POST | /api/auth/logout | No | Destroy session |
| GET | /api/auth/user | Yes | Get current user |
| POST | /api/auth/google/token | No | Google OAuth sign-in |
| GET | /api/auth/google/status | No | Check if Google OAuth is configured |
| POST | /api/auth/forgot-password | No | Request password reset email |
| POST | /api/auth/reset-password | No | Reset password with token |
### Threads & Messages (`server/routes.ts`)
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/threads | Yes | List user's threads |
| POST | /api/threads | Yes | Create thread + first AI reply |
| GET | /api/threads/:id | Yes | Get thread + all messages |
| DELETE | /api/threads/:id | Yes | Delete thread and messages |
| POST | /api/threads/:id/share | Yes | Generate share token |
| DELETE | /api/threads/:id/share | Yes | Revoke share token |
| POST | /api/threads/save-for-share | Yes | Save conversation for sharing |
| GET | /api/shared/:token | No | Get shared conversation (public) |
| POST | /api/threads/:id/messages | Yes | Send message to existing thread |
### Documents, Bookmarks, History
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/documents | Yes | List user's documents |
| POST | /api/documents | Yes | Upload/create document |
| DELETE | /api/documents/:id | Yes | Delete document |
| GET | /api/bookmarks | Yes | List bookmarks |
| POST | /api/bookmarks | Yes | Save bookmark |
| DELETE | /api/bookmarks/:id | Yes | Delete bookmark |
| GET | /api/search-history | Yes | List search history |
| POST | /api/search-history | Yes | Log a search query |
### Legal Search
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/statutes/search?q= | No | Search statutes DB |
| GET | /api/statute-documents/search?q= | No | Search statute documents |
| GET | /api/statute-documents/:id | No | Get full statute document |
| GET | /api/case-law/search?q= | No | Search case law DB |
| GET | /api/saved-judgments | Yes | List user's saved judgments |
| POST | /api/saved-judgments | Yes | Save a judgment |
| DELETE | /api/saved-judgments/:id | Yes | Delete saved judgment |
| GET | /api/statute-lookup?name=§ion= | Yes | Multi-source statute lookup |
### AI Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/ai/chat | Yes | Main AI chat (streaming or JSON) |
| POST | /api/ai/transcribe | Yes | Transcribe audio file to text |
| POST | /api/ai/search-judgments | Yes | AI-powered judgment search |
| POST | /api/ai/judgment-summary | Yes | AI analysis of a judgment |
| POST | /api/ai/search-statutes | Yes | AI-powered statute search |
| POST | /api/ai/summarize | Yes | Summarize search findings |
| POST | /api/ai/brief | Yes | Generate detailed legal brief |
| POST | /api/ai/document-chat | Yes | Chat about a specific document |
| POST | /api/statute-documents/:id/toc | Yes | Extract table of contents from statute |
### Apex AI
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/apex/models | Yes | List Apex models available to user's tier |
| POST | /api/apex/chat | Yes | Send message to Apex model |
### Profile & Usage
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/usage | Yes | Get monthly usage stats |
| GET | /api/profile | Yes | Get user profile |
| PATCH | /api/profile | Yes | Update name fields |
### Organization
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/org | Yes | Get user's organization |
| POST | /api/org | Yes (Enterprise) | Create organization |
| DELETE | /api/org/:id | Yes (Owner) | Delete organization |
| GET | /api/org/:id/members | Yes (Member) | List members |
| POST | /api/org/:id/invite | Yes (Owner) | Invite by email |
| GET | /api/org/:id/invites | Yes (Owner) | List pending invites |
| DELETE | /api/org/:id/members/:userId | Yes (Owner) | Remove member |
| GET | /api/org/invites/pending | Yes | Get own pending invites |
| POST | /api/org/invites/:id/accept | Yes | Accept invite |
| POST | /api/org/invites/:id/decline | Yes | Decline invite |
| GET | /api/org/:id/knowledge | Yes (Member) | List org knowledge docs |
| POST | /api/org/:id/knowledge | Yes (Member) | Upload org document |
| DELETE | /api/org/:id/knowledge/:docId | Yes (Owner) | Delete org document |
### Admin (all require isAdmin = true)
| Method | Path | Description |
|---|---|---|
| GET | /api/admin/check | Check if admin exists |
| POST | /api/admin/setup | Claim first admin |
| GET | /api/admin/users | List all users |
| POST | /api/admin/users | Create user |
| PATCH | /api/admin/users/:id | Update user tier/admin |
| DELETE | /api/admin/users/:id | Delete user and all data |
| GET | /api/admin/stats | System statistics |
| GET | /api/admin/cost-analytics | Cost breakdown by feature |
| GET/POST/DELETE | /api/admin/knowledge | Admin knowledge vault CRUD |
| GET/POST/DELETE | /api/admin/case-law | Case law database CRUD |
| POST | /api/admin/case-law/bulk | Bulk import case law |
| POST | /api/admin/case-law/extract | Extract case law from file |
| POST | /api/admin/case-law/auto-extract | Background AI extraction |
| GET/POST/DELETE | /api/admin/statute-documents | Statute documents CRUD |
---
## 11. Feature-by-Feature Flow
### 11.1 Registration & Login
**Registration (email/password)**:
1. User fills out form on `/auth` page
2. Frontend calls `POST /api/auth/register` with `{ email, password, firstName, lastName }`
3. Server checks if email already exists → 409 if duplicate
4. Password hashed with bcryptjs (12 rounds)
5. New `users` row inserted with `subscriptionTier = "free"`, `authProvider = "email"`
6. `req.session.userId = newUser.id` → session created
7. Response: user object (no passwordHash)
8. Frontend invalidates `/api/auth/user` query → React Query refetches → user is now logged in → redirect to `/`
**Login (email/password)**:
1. `POST /api/auth/login` with `{ email, password }`
2. Server finds user by email → 401 if not found
3. `bcrypt.compare(password, user.passwordHash)` → 401 if mismatch
4. If user's authProvider is "google" → 400 (use Google sign-in instead)
5. Session created, user returned
### 11.2 Main AI Chat (Al Wakeelo Engine)
**User action**: Types a message and hits Send on `/al-wakeelo`
**Frontend flow** (`chat.tsx` → `handleSend()`):
1. User message appended to local state immediately (optimistic UI)
2. Input cleared, loading state set
3. If Apex model selected → goes to Apex flow (see §11.8)
4. Otherwise → `POST /api/ai/chat` with:
- `messages`: Array of `{role, content}` (full conversation history)
- `type`: `"chat"` (or `"draft"`, `"contract-drafting"`)
- `turbo`: boolean (Pro/Enterprise users toggle this)
- `stream`: `true`
5. Response is an SSE stream
6. Frontend reads stream chunks with `ReadableStream.getReader()`
7. Each `data: {text: "..."}` chunk is appended to the assistant message in real time
8. Final `data: {done: true}` signals end of stream
9. After stream ends: logs to `/api/search-history`, invalidates `/api/usage`
**Backend flow** (`POST /api/ai/chat`):
1. `isAuthenticated` middleware → 401 if no session
2. `checkUsageLimit(userId, "chat", res)` → 429 if over limit
3. Parse request: handle both `multipart/form-data` (with files) and `application/json`
4. If files attached: extract text using `unpdf` (PDF) or `mammoth` (DOCX)
5. Build system prompt: `getLegalSystemPrompt()` + optional draft mode instruction
6. `gatherKnowledgeContext(lastUserMessage, userId)` → appends relevant legal content
7. If files: prepend extracted text as "ATTACHED DOCUMENTS FROM USER"
8. Check query cache (SHA-256 hash of first message only)
9. Select model: pro model if `turbo && tier in [pro, enterprise]`, flash otherwise
10. If `stream: true`:
- Set SSE headers
- Call `getAI().models.generateContentStream()`
- Forward each chunk as `data: {text: chunk}\n\n`
- On 429 from pro model: fallback to flash, re-stream
- End: `data: {done: true, model: "..."}\n\n`
11. `logUsageCost(userId, "chat", model, inputText, fullContent)`
### 11.3 Judgment Search
**User action**: Types a search query on `/judgment-search`
**Frontend flow**:
1. State: `query`, `localResults`, `externalResults`, `summaries`
2. On Search button click:
- Calls `GET /api/case-law/search?q=query` → local database results immediately
- Calls `POST /api/ai/search-judgments` with `{ query }` → AI-powered results
3. Results shown in two tabs: Local Database and AI Search
4. User can click "Get Analysis" on any result → `POST /api/ai/judgment-summary`
5. Summary modal opens with full AI analysis
6. User can save judgment → `POST /api/saved-judgments`
7. User can click judgment card → navigates to `/judgment-view` (data passed via sessionStorage)
**Backend - `/api/ai/search-judgments`**:
1. Check usage limit
2. Gather knowledge context
3. Call Gemini with `responseMimeType: "application/json"`
4. Prompt instructs AI to return array of `{citation, court, title, summary, keywords, uri}`
5. Parse JSON response, return array
**Backend - `/api/ai/judgment-summary`**:
1. Search `githubKnowledge` and `adminKnowledge` for the judgment's full text
2. If full text found: send to Gemini for detailed analysis based on actual text
3. If not found: Gemini generates analysis from general knowledge + disclaimer
4. Returns `{ summary, fullText, source, verified }`
### 11.4 Statute Search & Statute View
**Statute Search** (`/statute-search`):
1. User types in search bar (debounced 300ms)
2. `GET /api/statute-documents/search?q=query` returns matching statute documents
3. Dropdown suggestions shown
4. On selection: navigate to `/statute-view/:id`
5. Common statute buttons also available (hardcoded list, clicking searches for that statute)
**Statute View** (`/statute-view/:id`):
1. On mount: `GET /api/statute-documents/:id` fetches full statute text
2. `POST /api/statute-documents/:id/toc` → AI extracts table of contents structure from document text (returns JSON array of chapters/sections)
3. TOC displayed in left sidebar; clicking a section scrolls to that part of document
4. User can type in chat input at bottom → `POST /api/ai/document-chat` with `{ documentType, documentTitle, documentContent, messages }`
5. Response appended to chat in statute view
**Backend - TOC extraction**:
- Sends first 15,000 chars of statute to Gemini
- Prompt instructs JSON output of TOC structure: `[{title, level, children?}]`
- Response cached in `queryCache` (7-day TTL)
### 11.5 Legal Drafting
`/legal-drafting` renders `ChatModule` with `type="draft"`.
The only difference from regular chat:
- Backend appends to system prompt: `"You are in legal drafting mode. Help draft legal documents in proper Pakistani legal format."`
- Search history logged as type `"draft"`
- Bookmarks saved with type `"draft"`
### 11.6 Contract Drafting
`/contract-drafting` has a split layout:
- Left: List of 16 contract template types
- Right: `ChatModule` with `type="contract-drafting"`
**User selects a template**:
1. `selectedTemplate` state updated
2. `chatKey` incremented → forces ChatModule to re-mount and reset messages
3. `initialMessage` prop set to e.g. `"Draft an Employment Agreement for me."`
4. ChatModule auto-sends this as the first user message on mount
### 11.7 File Attachments & Audio Transcription
**File attachments**:
1. User clicks paperclip icon → `<input type="file">` triggers (accepts .txt, .pdf, .docx)
2. Up to 5 files can be attached; size validation done client-side
3. Files shown as preview chips in input area
4. On send: `FormData` constructed instead of JSON body
5. Backend uses `multer` to receive files
6. Text extraction:
- `.txt` → read directly
- `.pdf` → `unpdf` library extracts text from each page
- `.docx` → `mammoth` library extracts text
7. Extracted text prepended to AI context as "ATTACHED DOCUMENTS FROM USER"
**Audio transcription**:
1. User clicks microphone icon → `<input type="file">` accepts audio (mp3, wav, m4a, webm, ogg)
2. File sent to `POST /api/ai/transcribe`
3. Backend reads file as base64, sends to `gemini-2.0-flash` with `inlineData`
4. Gemini transcribes audio → returns text
5. Transcribed text inserted into the chat input field
6. User can review and edit before sending
### 11.8 Apex AI Models (Kimi K2.5)
**Availability check on mount**:
- `GET /api/apex/models` queried when chat page loads
- Returns `{ available: boolean, models: [...], tier: string }`
- If `available: false` (no MOONSHOT_API_KEY), dropdown is not shown
**User selects Apex model**:
1. "Apex Models" dropdown appears next to the Standard/Turbo toggle
2. User selects a model (e.g., Apex Pro)
3. `selectedApexModel` state set to model ID, `turboMode` set to false
4. UI shows model name in emerald green
**Sending a message with Apex**:
1. `handleSend()` detects `selectedApexModel` is set
2. `POST /api/apex/chat` with `{ model: "apex-pro", message: text }`
3. Backend validates tier access to that model → 403 if not allowed
4. Checks usage limit
5. Fetches conversation history from thread if `threadId` provided
6. Calls `chatWithApex({ model, messages, maxTokens })`
7. `chatWithApex()` in `apex-ai.ts`:
- Constructs OpenAI-compatible payload with `extra_body: { chat_template_kwargs: { thinking: bool } }`
- POSTs to Moonshot API at `https://api.moonshot.ai/v1/chat/completions`
- Returns `{ content, reasoning, model }`
8. Response JSON returned to frontend (not streaming)
9. Frontend handles via existing JSON response path (`data.content`)
### 11.9 Conversation Sharing
**User clicks Share**:
1. Button appears after 2+ messages in chat
2. Click → `POST /api/threads/save-for-share` if no threadId yet (saves in-memory conversation to DB first)
3. Then `POST /api/threads/:id/share` → server generates `crypto.randomBytes(16).toString("hex")` (32-char token)
4. Token stored in `threads.shareToken` column
5. Returns `{ shareToken, shareUrl: "/share/<token>" }`
6. Frontend shows shareable URL; copy button available
**Recipient visits share URL** (`/share/<token>`):
1. `GET /api/shared/:token` (no auth required)
2. Server finds thread by shareToken, fetches all messages
3. Returns `{ title, createdAt, messages[], sharedBy: "User Name" }`
4. Page renders read-only conversation view with reference cards
### 11.10 Bookmarks
**User bookmarks an AI response**:
1. Bookmark icon appears on each assistant message
2. Click → `POST /api/bookmarks` with `{ title: first 60 chars, content: full message, type: "al-wakeelo", category: "legal-advice" }`
3. Bookmark ID stored in local `bookmarkedIds` Set
4. Icon turns filled (saved state)
**Bookmarks page** (`/bookmarks`):
1. `GET /api/bookmarks` → list of all saved bookmarks
2. User can open preview modal, download as .txt, or delete (`DELETE /api/bookmarks/:id`)
### 11.11 Knowledge Vault (User Documents)
**Uploading a document**:
1. User selects file on `/knowledge-vault`
2. File read client-side with `FileReader.readAsText()`
3. `POST /api/documents` with `{ title: filename, content: fileText }`
4. Server sanitizes null bytes, inserts into `documents` table
**How documents are used in AI**:
- Every AI call triggers `gatherKnowledgeContext(query, userId)`
- The function searches `documents` table for content matching the query (ILIKE)
- Up to 2 matching excerpts (1500 chars each) appended to AI system prompt as "USER'S CASE DOCUMENTS"
### 11.12 Search History
Every AI interaction logs to `searchHistory`:
- `POST /api/search-history` called after each successful AI query
- `type`: one of `"chat"`, `"judgment"`, `"statute"`, `"draft"`, `"contract"`
- `query`: first 80 characters of the user's message
The `/history` page shows:
- Recent chat threads (horizontal scroll, click to open thread messages)
- Full search history table with type badge, query text, timestamp
### 11.13 Organization & Team Collaboration
**Creating an organization (Enterprise only)**:
1. `POST /api/org` with `{ name, description }`
2. Server checks user tier is `"enterprise"` or `isAdmin`
3. Checks user doesn't already have an org
4. Creates `organizations` row + `orgMembers` row (owner role)
**Inviting a member**:
1. Owner enters email in invite form
2. `POST /api/org/:id/invite` with `{ email }`
3. `orgInvites` row created with status `"pending"`
**Invited user sees pending invite**:
1. `GET /api/org/invites/pending` checks `orgInvites` WHERE email = current user's email AND status = "pending"
2. User sees invite with org name and owner info
3. Accept → `POST /api/org/invites/:id/accept`:
- Checks user doesn't already belong to an org
- Inserts `orgMembers` row
- Updates invite status to `"accepted"`
4. Decline → `POST /api/org/invites/:id/decline` → updates status
**Organization knowledge base**:
- Members upload files via `POST /api/org/:id/knowledge`
- Files extracted (PDF/DOCX/TXT), stored in `orgKnowledge` table
- `gatherKnowledgeContext()` always includes org knowledge if user is an org member (up to 3 results)
- Only the org owner can delete documents
**Priority API access for Enterprise**:
- `checkUsageLimit()` bypasses monthly limit for enterprise users
- `gatherKnowledgeContext()` always returns org knowledge for enterprise org members
- Pro model auto-selected for enterprise without needing turbo toggle
### 11.14 Admin Panel
The admin panel at `/admin` has 5 tabs:
**Analytics tab**:
- `GET /api/admin/stats` → total users, threads, messages, documents, cache entries, monthly queries
- `GET /api/admin/cost-analytics` → per-feature breakdown of queries, tokens, cost for current month
- Displayed as stat cards + feature cost table with progress bars
**Users tab**:
- `GET /api/admin/users` → all users listed
- Admin can change tier: `PATCH /api/admin/users/:id` with `{ subscriptionTier: "pro" }`
- Admin can toggle admin status (cannot remove own admin)
- Admin can delete user: `DELETE /api/admin/users/:id` → cascades and deletes all user data
**Knowledge Vault tab**:
- `GET/POST/DELETE /api/admin/knowledge`
- Admin uploads .txt, .pdf, .json, .csv files (up to 500 at once)
- Text extracted and stored in `adminKnowledge` table
- These documents feed into `gatherKnowledgeContext()` as high-priority knowledge
**Case Law tab**:
- Full CRUD on the `caseLaw` table
- Bulk import: JSON or CSV file with case law entries
- AI extraction: Upload a PDF/DOCX/TXT, server uses NLP to extract structured case law entries
- Auto-extract: Background job runs Gemini over synced GitHub documents to auto-populate case law
**Statute Library tab**:
- Full CRUD on the `statuteDocuments` table
- PDF/TXT files uploaded, text extracted
- Documents become searchable on `/statute-search` and viewable on `/statute-view`
### 11.15 Password Reset
1. User goes to `/forgot-password`, enters email
2. `POST /api/auth/forgot-password`:
- Finds user by email (returns same success message even if not found, for security)
- Generates 32-byte hex token, stores in `password_reset_tokens` (expires in 1 hour)
- Sends email via Resend API with reset link
3. User clicks link → `/reset-password?token=<token>`
4. `POST /api/auth/reset-password` with `{ token, password }`:
- Validates token exists, not expired, not used
- Hashes new password, updates `users.passwordHash`
- Marks token as used
5. Frontend redirects to `/auth`
---
## 12. Knowledge Context System
**File**: `server/routes.ts` → `gatherKnowledgeContext(query, userId)`
This is the core function that makes the AI know Pakistani law. Called before every AI request.
### Priority Order (highest to lowest)
1. **Internal Statutes DB** — `storage.searchStatutes(query, 3)` → searches `statutes` table
2. **Internal Case Law DB** — `storage.searchCaseLaw(query, 3)` → searches `caseLaw` table
3. **GitHub Legal Library** — searches `githubKnowledge` table (348+ synced legal documents)
4. **Admin Knowledge Vault** — searches `adminKnowledge` table
5. **User's Case Documents** — searches `documents` table filtered by userId
6. **Organization Knowledge** — searches `orgKnowledge` for org members
### Search Mechanism
- All searches use PostgreSQL ILIKE (case-insensitive substring match) on relevant text columns
- Results limited per tier to control token usage
- Each excerpt capped at 1,500 characters
### Output Format
```
=== INTERNAL KNOWLEDGE VAULT: STATUTES ===
[Short Title, Section]: Description. Punishment: ...
=== INTERNAL KNOWLEDGE VAULT: CASE LAW ===
Citation (Court): Title — Summary
=== CHAMBERS LEGAL LIBRARY (CURATED SOURCES) ===
[Filename]: Content excerpt...
=== CHAMBERS KNOWLEDGE VAULT (ADMIN UPLOADED) ===
[Title]: Content excerpt...
=== USER'S CASE DOCUMENTS ===
[Title]: Content excerpt...
=== ORGANIZATION KNOWLEDGE BASE ===
[Title]: Content excerpt...
```
### GitHub Sync (Startup Process)
`server/github-sync.ts`:
1. Fetches file list from GitHub API: `repos/ijlalbintariq/law/git/trees/HEAD?recursive=1`
2. Filters for `.txt` files
3. For each file not already in DB: fetches content, inserts into `githubKnowledge`
4. Skips if already synced (checks by filename)
5. 350 documents currently synced
---
## 13. Caching System
**Table**: `queryCache`
**TTL**: 7 days
**Hash**: SHA-256 of normalized query text (lowercase, trimmed)
### What Gets Cached
- First message in a new chat thread
- Standalone AI chat (first message only)
- Judgment searches
- Statute searches
- Summaries and briefs
- TOC extraction
Follow-up messages in a thread are NOT cached (they depend on conversation context).
### Cache Flow
```
Request arrives
↓
Normalize query → SHA-256 hash
↓
SELECT from queryCache WHERE queryHash = hash AND endpoint = feature
↓
Found & not expired?
├── Yes → Return cached response, INCREMENT hitCount
└── No → Call Gemini API → Store result in queryCache → Return result
```
### Cache Cleanup
- On server startup: `DELETE FROM queryCache WHERE createdAt < NOW() - INTERVAL 7 DAYS`
- Daily interval job repeats the cleanup
---
## 14. Cost Tracking & Analytics
### Token Estimation
```typescript
estimateTokens(text: string): number {
return Math.ceil(text.length / 4); // ~4 chars per token
}
```
### Cost Rates (USD per 1K tokens)
| Model | Input | Output |
|---|---|---|
| gemini-3-flash-preview | $0.00015 | $0.0006 |
| gemini-3-pro-preview | $0.00125 | $0.005 |
### Analytics Aggregation
`GET /api/admin/cost-analytics` runs:
```sql
SELECT feature,
COUNT(*) as totalQueries,
SUM(inputTokens) as totalInputTokens,
SUM(outputTokens) as totalOutputTokens,
SUM(CAST(estimatedCost AS DECIMAL)) as totalCost
FROM usageTracking
WHERE createdAt >= start_of_current_month
GROUP BY feature
```
---
## 15. PWA & Install Guide
### Service Worker (`client/public/sw.js`)
- Network-first strategy for HTML pages
- Cache fallback for offline access
- Caches static assets on install
### Manifest (`client/public/manifest.json`)
- App name: "Al Wakeelo"
- Theme color: amber/gold
- Display mode: `standalone` (no browser chrome)
- Icons defined for various sizes
### Install Guide Page (`/install`)
A standalone page (no auth required) that shows step-by-step installation instructions for:
- **iOS**: Open in Safari → Share button → "Add to Home Screen" → Add
- **Android**: Open in Chrome → Three-dot menu → "Add to Home Screen" → Install
- **Desktop**: Open in Chrome/Edge → Install icon in address bar → Install
---
## 16. Startup Processes
When the server starts (`server/index.ts`):
1. **Express app initialized** — sessions, body parsers, multer, passport, CORS
2. **Database connection** — Drizzle ORM connects via `DATABASE_URL`
3. **Schema push** — Tables created/synced if not existing
4. **Query cache cleanup** — Expired entries (>7 days) deleted
5. **Search index creation** — PostgreSQL text search indexes verified/created on key tables
6. **GitHub Sync** — `syncGitHubKnowledge()` runs:
- Fetches file tree from GitHub API
- Inserts any new `.txt` files not already in `githubKnowledge`
- Skips if already synced (logs: "Knowledge base already synced (350 documents)")
7. **Vite dev middleware** — In development, Vite HMR middleware integrated into Express
8. **Routes registered** — All auth, API, and static file routes registered
9. **Server listens** on port 5000
---
## 17. Build & Deployment
### Development
```bash
npm run dev
# Runs: NODE_ENV=development tsx server/index.ts
# Vite dev middleware serves frontend with HMR
# Backend serves on port 5000
```
### Production Build
```bash
npm run build
# script/build.ts runs:
# 1. Vite build → dist/public/ (frontend SPA)
# 2. esbuild → dist/server.js (backend bundle)
```
### Production Start
```bash
npm run start
# Runs: node dist/server.js
# Serves frontend static files from dist/public/
# All routes handled by the single Express server
```
### Database Migrations
```bash
npm run db:push
# Runs: drizzle-kit push
# Syncs shared/schema.ts to the live PostgreSQL database
# Safe for development — does not drop data
```
### Environment Notes
- In development, Vite dev server runs as Express middleware (same port 5000)
- In production, Express serves the Vite-built static files directly
- All environment variables must be set before starting (see §4)
- The `sessions` and `users` tables must never be dropped — they store active sessions
---
*This document covers the complete technical architecture of Al Wakeelo as of February 2026. For changes, update both this file and `replit.md`.*