A full-stack platform connecting clients with verified lawyers in Bangladesh
Features β’ Architecture β’ API Docs β’ Setup β’ Frontend Integration
LegalAid is a two-sided legal marketplace built specifically for Bangladesh. Think of it as a combination of Fiverr (hire lawyers), Clio (case management), and a secure document vault β all in one platform.
Clients can browse verified lawyers, hire them through a structured contract flow, manage their legal cases, and pay securely via bKash escrow.
Lawyers can list their services, accept hire requests, manage active contracts and cases, communicate with clients, and withdraw their earnings.
Admins verify lawyer credentials, handle payment disputes, and monitor platform activity.
- π Browse and search verified lawyers by practice area, location, and rating
- π Hire lawyers through a structured contract flow
- π³ Pay via bKash with escrow protection
- π Manage legal cases with milestones and document uploads
- π¬ Message lawyers directly within contracts
- β Leave reviews after contract completion
- βοΈ Raise disputes if work is unsatisfactory
- π§βπΌ Create and manage service listings
- β Get verified through document submission (bar cert + NID)
- π Track earnings, contracts, and reviews
- π Access case files shared by clients
- π° Request payouts to bKash or bank account
- π Review and approve/reject lawyer verifications
- π΅ Verify bKash transaction IDs and activate escrow
- βοΈ Resolve disputes with full evidence and message history
- π Monitor platform-wide statistics
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Frontend β
β React 18 + Vite + Tailwind CSS β
β (Port 5173 in dev) β
βββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
β HTTP / REST
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Spring Boot Backend β
β Modular Monolith (Port 8080) β
β β
β ββββββββββββ ββββββββββββ ββββββββββββββββ β
β β auth/ β β user/ β β lawyer/ β β
β β JWT + β β profiles β β verification β β
β β OAuth β ββββββββββββ ββββββββββββββββ β
β ββββββββββββ β
β ββββββββββββ ββββββββββββ ββββββββββββββββ β
β β service/ β β contract/β β payment/ β β
β βlistings β β escrow β β bKash TxnID β β
β ββββββββββββ ββββββββββββ ββββββββββββββββ β
β ββββββββββββ ββββββββββββ ββββββββββββββββ β
β β case_/ β β dispute/ β β review/ β β
β βmanagementβ βresolutionβ β ratings β β
β ββββββββββββ ββββββββββββ ββββββββββββββββ β
β ββββββββββββ ββββββββββββ ββββββββββββββββ β
β βdocument/ β β message/ β βnotification/ β β
β βCloudinaryβ β threads β β in-app β β
β ββββββββββββ ββββββββββββ ββββββββββββββββ β
β ββββββββββββ ββββββββββββ β
β β search/ β β admin/ β β
β β unified β βdashboard β β
β ββββββββββββ ββββββββββββ β
βββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
β
ββββββββββββ΄βββββββββββ
βΌ βΌ
βββββββββββββββ ββββββββββββββββ
β PostgreSQL β β Cloudinary β
β (Primary β β (Documents, β
β Database) β β Images, β
β β β Videos) β
βββββββββββββββ ββββββββββββββββ
| Pattern | Where Applied |
|---|---|
| Modular Monolith | Whole application |
| Layered Architecture | All 16 packages (Controller β Service β Repository) |
| Repository Pattern | All data access via Spring Data JPA |
| DTO Pattern | Separate request/response shapes from DB entities |
| State Machine | Contract lifecycle, Dispute flow |
| Soft Delete | All major tables via deleted_at |
| Snapshot Pattern | Service data copied to contract at hire time |
| Observer + @Async | Notification system decoupled from business logic |
| Guard Clause | Fail-fast validation at top of every service method |
| Technology | Version | Purpose |
|---|---|---|
| Java | 21 | Primary language |
| Spring Boot | 3.3.x | Application framework |
| Spring Security | 6.x | Auth + endpoint guards |
| Spring Data JPA | 3.x | ORM and repositories |
| PostgreSQL | 15+ | Primary database |
| JWT (JJWT) | 0.12.6 | Stateless auth tokens |
| Google OAuth 2.0 | β | Social login |
| Cloudinary | 1.39.0 | File storage |
| HikariCP | Built-in | Connection pooling |
| JUnit 5 + Mockito | β | Testing |
| Technology | Version | Purpose |
|---|---|---|
| React | 18 | UI framework |
| Vite | Latest | Build tool |
| Tailwind CSS | 3.x | Styling |
| React Router DOM | 6 | Client-side routing |
| Lucide React | β | Icons |
backend/
βββ src/main/java/com/legalaid/
βββ LegalAidApplication.java
βββ config/
β βββ SecurityConfig.java # JWT filter chain, public endpoints
β βββ JwtConfig.java # Token expiry + secret
β βββ CorsConfig.java # Allowed origins
β βββ CloudinaryConfig.java # Cloudinary bean
βββ common/
β βββ response/ApiResponse.java # Standard { success, data, message }
β βββ exception/GlobalExceptionHandler.java
βββ auth/ # Google OAuth + JWT
βββ user/ # User profiles + role management
βββ lawyer/ # Profiles, verification, search
βββ service/ # Service listings + media
βββ contract/ # Hire flow + state machine
βββ case_/ # Case management + access control
βββ payment/ # bKash escrow + payouts
βββ message/ # Contract messaging
βββ notification/ # In-app notifications
βββ document/ # Cloudinary file uploads
βββ review/ # Ratings + helpful votes
βββ dispute/ # Dispute resolution
βββ search/ # Unified search
βββ admin/ # Admin dashboard
- Java 21+
- Maven 3.8+
- PostgreSQL 15+
- A Google Cloud project with OAuth 2.0 credentials
- A Cloudinary account (free tier works)
git clone https://github.com/chatok-jnr/legalaid-marketplace.git
cd legalaid-marketplace# Create the database
psql -U postgres -c "CREATE DATABASE legalaid;"
# Run the schema
psql -U postgres -d legalaid -f legalaid_schema.sql
psql -U postgres -d legalaid -f legalaid_migration_v2.sqlCreate a .env file in the backend/ directory:
# Database
DB_URL=jdbc:postgresql://localhost:5432/legalaid
DB_USERNAME=postgres
DB_PASSWORD=yourpassword
# JWT
JWT_SECRET=your-256-bit-secret-at-least-32-characters-long
# Google OAuth
GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
# Cloudinary
CLOUDINARY_CLOUD_NAME=your-cloud-name
CLOUDINARY_API_KEY=your-api-key
CLOUDINARY_API_SECRET=your-api-secretOr update src/main/resources/application.yml directly:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/legalaid
username: postgres
password: yourpassword
servlet:
multipart:
enabled: true
max-file-size: 50MB
max-request-size: 55MB
app:
cors:
allowed-origins: http://localhost:5173
jwt:
secret: your-256-bit-secret
access-token-expiry: 900000 # 15 minutes
refresh-token-expiry: 604800000 # 7 days
google:
client-id: your-google-client-id
cloudinary:
cloud-name: your-cloud-name
api-key: your-api-key
api-secret: your-api-secretcd backend
./mvnw spring-boot:runBackend will start at http://localhost:8080
cd frontend
npm install
npm run devFrontend will start at http://localhost:5173
The frontend uses Google Sign-In. After the user authenticates with Google, the frontend receives a Google ID token and sends it to the backend:
// After Google Sign-In success
const response = await fetch('http://localhost:8080/api/auth/google', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ idToken: googleIdToken }),
credentials: 'include' // important β needed for refresh token cookie
});
const { data } = await response.json();
// data.accessToken β store in memory (NOT localStorage)
// data.role β CLIENT, LAWYER, or ADMIN
// data.isNewUser β true if first login (redirect to onboarding)
// refresh token is automatically stored in HttpOnly cookieInclude the access token in every protected request:
const response = await fetch('http://localhost:8080/api/contracts', {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
});Access tokens expire in 15 minutes. Refresh automatically:
// POST /api/auth/refresh
// No body needed β refresh token is sent automatically via cookie
const response = await fetch('http://localhost:8080/api/auth/refresh', {
method: 'POST',
credentials: 'include'
});
const { data } = await response.json();
// data.accessToken β new access tokenEvery endpoint returns the same shape:
{
"success": true,
"data": { },
"message": null,
"timestamp": "2026-04-01T10:00:00Z"
}On error:
{
"success": false,
"data": null,
"message": "Contract cannot be cancelled β current status: COMPLETED",
"timestamp": "2026-04-01T10:00:00Z"
}Use multipart/form-data for document uploads:
const formData = new FormData();
formData.append('file', selectedFile);
formData.append('folderName', 'case-documents'); // optional
formData.append('caseId', caseId); // optional
const response = await fetch('http://localhost:8080/api/documents/upload', {
method: 'POST',
headers: { 'Authorization': `Bearer ${accessToken}` },
body: formData
// DO NOT set Content-Type β browser sets it automatically with boundary
});| Method | Endpoint | Access | Description |
|---|---|---|---|
| POST | /api/auth/google |
Public | Login with Google ID token |
| POST | /api/auth/refresh |
Cookie | Refresh access token |
| POST | /api/auth/logout |
Auth | Clear refresh token cookie |
| GET | /api/auth/me |
Auth | Get current user ID |
| Method | Endpoint | Access | Description |
|---|---|---|---|
| GET | /api/users/me |
Auth | Own full profile |
| PUT | /api/users/me |
Auth | Update profile |
| GET | /api/users/:id |
Public | Public profile |
| PUT | /api/users/me/request-lawyer |
CLIENT | Request lawyer role |
| DELETE | /api/users/me |
Auth | Soft delete account |
| Method | Endpoint | Access | Description |
|---|---|---|---|
| GET | /api/lawyers |
Public | Browse verified lawyers |
| GET | /api/lawyers/:id |
Public | Lawyer public profile |
| GET | /api/lawyers/me |
LAWYER | Own profile |
| GET | /api/lawyers/me/stats |
LAWYER | Earnings + stats |
| PUT | /api/lawyers/me/onboarding |
CLIENT/LAWYER | Complete onboarding |
| POST | /api/lawyers/me/verify |
CLIENT/LAWYER | Submit verification docs |
| GET | /api/lawyers/:id/reviews |
Public | Lawyer reviews |
| Method | Endpoint | Access | Description |
|---|---|---|---|
| GET | /api/services |
Public | Browse services |
| GET | /api/services/:id |
Public | Service detail |
| GET | /api/services/mine |
LAWYER | Own services |
| GET | /api/services/by-lawyer/:id |
Public | Services by lawyer |
| POST | /api/services |
LAWYER | Create service |
| PUT | /api/services/:id |
LAWYER | Update service |
| DELETE | /api/services/:id |
LAWYER | Delete service |
| Method | Endpoint | Access | Description |
|---|---|---|---|
| GET | /api/contracts |
Auth | Own contracts |
| GET | /api/contracts/:id |
Participant | Contract detail |
| POST | /api/contracts |
CLIENT | Hire a lawyer |
| PUT | /api/contracts/:id/accept |
LAWYER | Accept request |
| PUT | /api/contracts/:id/decline |
LAWYER | Decline request |
| PUT | /api/contracts/:id/complete |
CLIENT | Mark complete |
| PUT | /api/contracts/:id/cancel |
Participant | Cancel contract |
| PUT | /api/contracts/:id/milestones/:mId |
Participant | Toggle milestone |
| Method | Endpoint | Access | Description |
|---|---|---|---|
| GET | /api/cases |
Auth | Own + accessible cases |
| GET | /api/cases/:id |
Access | Case detail |
| POST | /api/cases |
Auth | Create case |
| PUT | /api/cases/:id |
Owner/Editor | Update case |
| DELETE | /api/cases/:id |
Owner | Delete case |
| POST | /api/cases/:id/access |
Owner | Invite user |
| DELETE | /api/cases/:id/access/:userId |
Owner | Remove access |
| PUT | /api/cases/:id/milestones/:mId |
Owner/Editor | Toggle milestone |
| Method | Endpoint | Access | Description |
|---|---|---|---|
| POST | /api/payments |
CLIENT | Submit bKash TxnID |
| GET | /api/payments |
CLIENT | Payment history |
| GET | /api/payments/:id/invoice |
CLIENT | Download invoice |
| GET | /api/payments/balance |
LAWYER | Available balance |
| POST | /api/payouts/request |
LAWYER | Request withdrawal |
| GET | /api/payouts |
LAWYER | Payout history |
| Method | Endpoint | Access | Description |
|---|---|---|---|
| GET | /api/contracts/:id/messages |
Participant | Message thread |
| POST | /api/contracts/:id/messages |
Participant | Send message |
| GET | /api/messages/unread-count |
Auth | Unread badge count |
| Method | Endpoint | Access | Description |
|---|---|---|---|
| GET | /api/notifications |
Auth | Paginated notifications |
| GET | /api/notifications/unread-count |
Auth | Unread count |
| PUT | /api/notifications/:id/read |
Auth | Mark as read |
| PUT | /api/notifications/read-all |
Auth | Mark all read |
| DELETE | /api/notifications/:id |
Auth | Delete notification |
| Method | Endpoint | Access | Description |
|---|---|---|---|
| POST | /api/documents/upload |
Auth | Upload file |
| GET | /api/documents |
Auth | Own documents |
| GET | /api/cases/:id/documents |
Auth | Case documents |
| GET | /api/contracts/:id/documents |
Auth | Contract documents |
| PUT | /api/documents/:id |
Owner | Update metadata |
| PUT | /api/documents/:id/star |
Owner | Toggle starred |
| DELETE | /api/documents/:id |
Owner | Delete document |
| Method | Endpoint | Access | Description |
|---|---|---|---|
| POST | /api/reviews |
CLIENT | Submit review |
| PUT | /api/reviews/:id/reply |
LAWYER | Reply to review |
| POST | /api/reviews/:id/helpful |
Auth | Toggle helpful vote |
| Method | Endpoint | Access | Description |
|---|---|---|---|
| GET | /api/disputes |
Auth | Own disputes |
| GET | /api/disputes/:id |
Participant | Dispute detail |
| POST | /api/disputes |
Participant | Raise dispute |
| POST | /api/disputes/:id/evidence |
Participant | Upload evidence |
| POST | /api/disputes/:id/messages |
Participant | Send message |
| PUT | /api/disputes/:id/respond |
Other party | Respond to dispute |
| Method | Endpoint | Access | Description |
|---|---|---|---|
| GET | /api/search |
Public | Search lawyers + services |
| Method | Endpoint | Access | Description |
|---|---|---|---|
| GET | /api/admin/lawyers/pending |
ADMIN | Verification queue |
| PUT | /api/admin/lawyers/:id/verify |
ADMIN | Approve/reject lawyer |
| GET | /api/admin/payments/pending |
ADMIN | Pending TxnID verifications |
| PUT | /api/admin/payments/:id/verify |
ADMIN | Verify payment |
| PUT | /api/admin/payments/:id/reject |
ADMIN | Reject payment |
| GET | /api/admin/payouts/pending |
ADMIN | Pending payouts |
| PUT | /api/admin/payouts/:id/process |
ADMIN | Process payout |
| PUT | /api/admin/payouts/:id/reject |
ADMIN | Reject payout |
| GET | /api/admin/disputes |
ADMIN | All disputes |
| PUT | /api/admin/disputes/:id/resolve |
ADMIN | Resolve dispute |
| GET | /api/admin/stats |
ADMIN | Platform statistics |
Client hires lawyer
β
βΌ
PENDING_LAWYER ββββ lawyer declines βββΊ CANCELLED
β β²
β lawyer accepts β lawyer cancels (only here)
βΌ β
PENDING_PAYMENT βββββββββββββ
β β²
β payment verified β client cancels
βΌ β
ACTIVE ββββββββββββββββββ
β
βββ client confirms βββΊ COMPLETED βββΊ payment RELEASED to lawyer
β
βββ client disputes βββΊ DISPUTED βββΊ payment FROZEN
β β
β admin resolves
β β± β²
β REFUND RELEASE
β (CANCELLED) (COMPLETED)
β
βββ client cancels βββΊ CANCELLED βββΊ payment REFUNDED
1. Client sends money to LegalAid bKash merchant number manually
2. Client submits TxnID via POST /api/payments
3. Payment saved with status = PENDING_VERIFICATION
4. Admin verifies TxnID against bKash records manually
5. Admin calls PUT /api/admin/payments/:id/verify
6. Payment β HELD, Contract β ACTIVE
7. Work completed β client calls PUT /api/contracts/:id/complete
8. Payment β RELEASED, lawyer balance updated
9. Lawyer calls POST /api/payouts/request
10. Admin processes payout manually and calls PUT /api/admin/payouts/:id/process
# All tests
./mvnw test
# Specific test class
./mvnw test -Dtest=JwtServiceTest
./mvnw test -Dtest=AuthServiceTest
./mvnw test -Dtest=LawyerServiceTest
# All tests with coverage report
./mvnw test jacoco:report
# Report at: target/site/jacoco/index.html| Variable | Required | Description |
|---|---|---|
DB_URL |
β | PostgreSQL connection URL |
DB_USERNAME |
β | Database username |
DB_PASSWORD |
β | Database password |
JWT_SECRET |
β | Min 32 chars β used to sign tokens |
GOOGLE_CLIENT_ID |
β | From Google Cloud Console |
CLOUDINARY_CLOUD_NAME |
β | From Cloudinary dashboard |
CLOUDINARY_API_KEY |
β | From Cloudinary dashboard |
CLOUDINARY_API_SECRET |
β | From Cloudinary dashboard |
35 tables across 10 groups:
Users β users, client_profiles, lawyer_profiles, admin_profiles
β lawyer_locations, lawyer_practice_areas, lawyer_courts
β lawyer_languages, lawyer_availability, lawyer_verification_docs
Services β services, service_media, service_highlights
β service_features, service_faqs
Contracts β contracts, contract_milestones
Cases β cases, case_access, case_milestones, case_tags
Documents β documents
Messages β messages
Notifications β notifications
Payments β payments, payouts
Reviews β reviews, review_aspects, review_tags, review_helpful_votes
Disputes β disputes, dispute_evidence, dispute_messages, dispute_timeline
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Md. Sakib Hosen
- GitHub: @chatok-jnr
- LinkedIn: chatok-junior
- Portfolio: iamchatokjunior.netlify.app
This project is licensed under the MIT License β see the LICENSE file for details.