A multi-tenant SaaS platform built for Pakistani law firms and legal departments. LTS digitizes case management, hearing tracking, document storage, and automated alerts — replacing manual paper-based workflows.
- Overview
- Tech Stack
- Architecture
- Project Structure
- Features
- Getting Started
- Environment Variables
- API Documentation
- Database
- Background Jobs
- Authentication
- Multi-Tenancy
- Contributing
LTS is a multi-tenant SaaS application where each law firm (organization) operates in complete isolation. The system supports:
- SuperAdmin — manages all organizations, plans, and subscriptions across the platform
- Admin — manages their organization's users, cases, and settings
- User — manages cases, hearings, documents, and petitioners within their organization
| Category | Technology |
|---|---|
| Framework | ASP.NET 10 Web API |
| Language | C# |
| ORM | Entity Framework Core |
| Database | PostgreSQL (Neon) |
| Architecture | Vertical Slice + CQRS + MediatR |
| Authentication | JWT Bearer Tokens |
| Password Hashing | IPasswordHasher (ASP.NET Identity) |
| File Storage | Cloudinary |
| Background Jobs | Hangfire |
| Logging | Serilog |
| Validation | FluentValidation |
| API Documentation | Scalar |
| MailKit |
LTS follows Vertical Slice Architecture combined with CQRS and MediatR.
- Code is organized by feature, not by layer
- Every feature is a self-contained slice — its own commands, queries, handlers, validators, DTOs, mappings, and controller
- CQRS separates reading from writing — Commands change data, Queries read data, they never mix
- MediatR acts as the dispatcher — controllers send a Command or Query to MediatR, MediatR finds the correct handler and runs it
- DbContext is used directly inside handlers — no repository pattern
- Extension methods handle all entity-to-DTO and DTO-to-entity mappings — no AutoMapper
- No class libraries — single LTS.API project
React Frontend
↓
POST /api/Feature/ActionName
↓
Controller → _mediator.Send(command)
↓
MediatR Pipeline
↓
ValidationBehavior (FluentValidation) → 400 if invalid
↓
LoggingBehavior (Serilog)
↓
Handler → AppDbContext → PostgreSQL
↓
Response → ApiResponse<T> { Success, Data, Message }
↓
React Frontend
Routes follow /api/Feature/ActionName convention — not RESTful lowercase:
POST /api/Cases/CreateCase
GET /api/Cases/GetAllCases
GET /api/Cases/GetCaseById/{id}
PUT /api/Cases/UpdateCase
DELETE /api/Cases/DeleteCase/{id}
LTS.API/
├── Features/
│ ├── Cases/
│ │ ├── Commands/
│ │ │ ├── CreateCase/
│ │ │ │ ├── CreateCaseCommand.cs
│ │ │ │ ├── CreateCaseHandler.cs
│ │ │ │ └── CreateCaseValidator.cs
│ │ │ ├── UpdateCase/
│ │ │ └── DeleteCase/
│ │ ├── Queries/
│ │ │ ├── GetAllCases/
│ │ │ ├── GetCaseById/
│ │ │ └── SearchCases/
│ │ ├── Mappings/
│ │ │ └── CaseMappings.cs
│ │ └── CasesController.cs
│ ├── Courts/
│ ├── Departments/
│ ├── Petitioners/
│ ├── Followup/
│ ├── Documents/
│ ├── Bench/
│ ├── Auth/
│ ├── Alerts/
│ ├── Reports/
│ └── SuperAdmin/
│
├── Domain/
│ ├── Entities/
│ │ ├── BaseEntity.cs
│ │ ├── Case.cs
│ │ ├── Court.cs
│ │ ├── Department.cs
│ │ ├── Petitioner.cs
│ │ ├── CasePetitioner.cs
│ │ ├── Followup.cs
│ │ ├── Bench.cs
│ │ ├── CaseDocument.cs
│ │ └── User.cs
│ └── Enums/
│ ├── CaseStatus.cs
│ └── UserRole.cs
│
├── Infrastructure/
│ ├── Persistence/
│ │ ├── AppDbContext.cs
│ │ ├── Migrations/
│ │ └── Configurations/
│ ├── Services/
│ │ ├── Interfaces/
│ │ │ ├── IEmailService.cs
│ │ │ └── IFileStorageService.cs
│ │ ├── EmailService.cs
│ │ └── FileStorageService.cs
│ └── BackgroundJobs/
│ └── HearingAlertJob.cs
│
├── Common/
│ ├── Behaviors/
│ │ ├── ValidationBehavior.cs
│ │ └── LoggingBehavior.cs
│ ├── Middleware/
│ │ └── ExceptionHandlingMiddleware.cs
│ ├── Response/
│ │ └── ApiResponse.cs
│ └── Exceptions/
│ ├── NotFoundException.cs
│ └── UnauthorizedException.cs
│
├── appsettings.json
├── appsettings.Development.json
└── Program.cs
| Feature | Description |
|---|---|
| Auth | Register, Login, JWT token issuance, OTP email verification, password reset |
| Cases | Full CRUD, case search, status management, email list per case |
| Courts | Court management with address and contact details |
| Departments | Department management linked to organizations |
| Petitioners | Petitioner profiles linked to cases (many-to-many) |
| Followup / Hearings | Hearing dates, interim orders, decisions, next hearing tracking |
| Documents | File upload/download via Cloudinary, linked to cases |
| Bench | Judge assignment per case with contact details |
| Alerts | Automated hearing alert emails 3 days before hearing date (Hangfire) |
| Reports | Department-wise and court-wise case summary reports |
| SuperAdmin | Organization management, plan assignment, subscription control |
- .NET 10 SDK
- PostgreSQL or a Neon account
- Cloudinary account
- Git
git clone https://github.com/fayaz921/LTS-API.git
cd LTS-APIdotnet restoreCopy the example settings and fill in your values (see Environment Variables):
# Update appsettings.Development.json with your valuesdotnet ef database updatedotnet runAPI will be available at https://localhost:7001
Scalar documentation at https://localhost:7001/scalar
Configure the following in appsettings.Development.json:
{
"ConnectionStrings": {
"DefaultConnection": "Host=...;Database=lts;Username=...;Password=..."
},
"JwtSettings": {
"Secret": "your-secret-key-min-32-chars",
"Issuer": "LTS-API",
"Audience": "LTS-Client",
"ExpiryInMinutes": 60
},
"CloudinarySettings": {
"CloudName": "your-cloud-name",
"ApiKey": "your-api-key",
"ApiSecret": "your-api-secret"
},
"EmailSettings": {
"Host": "smtp.gmail.com",
"Port": 587,
"Username": "your-email@gmail.com",
"Password": "your-app-password",
"FromName": "LTS System"
},
"SuperAdmin": {
"Email": "superadmin@lts.com",
"Password": "your-superadmin-password"
}
}Never commit
appsettings.Development.jsonto version control. It is already in.gitignore.
API documentation is available via Scalar (not Swagger):
https://localhost:7001/scalar
All endpoints are grouped by feature and fully documented with request/response schemas.
- Provider: PostgreSQL via Neon (serverless)
- ORM: Entity Framework Core with code-first migrations
- Configurations: Each entity has its own
IEntityTypeConfigurationclass underInfrastructure/Persistence/Configurations/
dotnet ef migrations add MigrationName
dotnet ef database updateThe following columns are indexed for query performance:
OrganizationId— on every entity (multi-tenancy filtering)Status— on CasesDateInstitution— on CasesHearingDate— on Followup
Hangfire is used for background job scheduling:
| Job | Schedule | Description |
|---|---|---|
HearingAlertJob |
Daily | Sends email alerts for hearings within the next 3 days |
SubscriptionExpiryJob |
Daily | Sends email when organization subscription is about to expire |
Hangfire dashboard is available at /hangfire (SuperAdmin only).
- JWT Bearer tokens issued on login
- Tokens include claims:
UserId,OrganizationId,Role - All protected endpoints require
Authorization: Bearer <token>header - Roles:
SuperAdmin,Admin,User - Passwords hashed using ASP.NET
IPasswordHasher
LTS is a multi-tenant SaaS — each organization's data is fully isolated:
- Every entity carries an
OrganizationIdforeign key CurrentUserServiceextractsOrganizationIdfrom the JWT token on every request- Every handler filters queries by
OrganizationId— no cross-organization data leakage - SuperAdmin role bypasses tenant filtering to manage all organizations
This is a private team project. Branch and PR rules:
- Never push directly to
main - Create a feature branch:
Feature/YourFeatureName - Open a Pull Request — CI must pass before merge
- PR must be reviewed and approved by the Tech Lead before merging
Feature/CreateCase
Feature/UpdateCourt
Feature/AddReports
- Entities live in
Domain/Entitiesonly — never inside Features - DTOs and Commands live inside their feature slice only — never shared between features
- Use
AppDbContextdirectly inside handlers — no repositories - Use extension methods in
Mappings/for all mapping — no AutoMapper - Controllers must be thin — one line only:
_mediator.Send() - Every Command must have a FluentValidation Validator
- Always use
AsNoTracking()on GET queries - Always filter by
OrganizationIdon every query
| Repo | Description |
|---|---|
| LTS-Client | React + TypeScript frontend dashboard |
| LTS-Landing | Next.js landing page (coming soon) |
Litigation Tracking System — Built for Pakistani Law Firms