A production-ready, multi-tenant CRM system built with Django REST Framework, React, PostgreSQL, and AWS S3 integration.
This is a B2B multi-tenant CRM platform designed for the Associate Full Stack Developer Technical Examination. The system enables multiple organizations (tenants) to operate independently within a single shared infrastructure while maintaining strict data isolation and security.
- ✅ Multi-tenant architecture with organization-level data isolation
- ✅ Role-based access control (Admin, Manager, Staff)
- ✅ JWT authentication with secure token management
- ✅ Company & Contact management with full CRUD operations
- ✅ Activity logging - Automatic audit trail for all CRUD operations
- ✅ Soft delete - Data preservation with
is_deletedflag - ✅ AWS S3 integration - Secure file storage with signed URLs
- ✅ Pagination, Search & Filtering - Advanced data querying
- ✅ Email validation - Format checking and uniqueness per company
- ✅ Phone validation - 8-15 digit format validation
- ✅ API versioning -
/api/v1/routing structure - ✅ Production-ready - Proper environment configuration and security
The platform uses a single database, shared schema approach:
┌─────────────────────────────────────────┐
│ Shared Database │
│ ┌──────────┐ ┌──────────┐ │
│ │ Org A │ │ Org B │ │
│ │ Users │ │ Users │ Isolated │
│ │ Companies│ │ Companies│ Data │
│ │ Contacts │ │ Contacts │ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────────┘
Tenant Enforcement:
- Every model has an
organizationForeignKey - All queries automatically filter by
request.user.organization - Middleware and permission classes enforce isolation
- No user can access another organization's data
Backend:
- Django 4.2.x
- Django REST Framework
- PostgreSQL 15+
- JWT Authentication (djangorestframework-simplejwt)
- AWS S3 (django-storages + boto3)
- Pillow (image processing)
Frontend:
- React 19 with Vite
- React Router DOM (protected routes)
- Axios (API communication)
- Context API (state management)
- Features
- Architecture
- Installation
- Configuration
- API Endpoints
- Frontend Pages
- Database Models
- Security
- Project Structure
- Key Implementation Highlights
- Repository
- Python 3.11+
- PostgreSQL 15+
- Node.js 18+
- npm or yarn
- Docker Desktop (optional, for PostgreSQL)
git clone https://github.com/Inkithai/CRM.git
cd CRM# Start PostgreSQL container
docker-compose up -d- Install PostgreSQL from postgresql.org
- Create database and user:
CREATE DATABASE crm_db;
CREATE USER crm_user WITH PASSWORD 'your_secure_password';
GRANT ALL PRIVILEGES ON DATABASE crm_db TO crm_user;cd backend
# Create virtual environment
python -m venv venv
# Activate virtual environment
# Windows:
venv\Scripts\activate
# Linux/Mac:
source venv/bin/activate
# Install dependencies
pip install -r requirements.txt
# Copy environment file
copy .env.example .env # Windows
cp .env.example .env # Linux/Mac
# Edit .env with your credentialspython manage.py migrate
# Create superuser (optional)
python manage.py createsuperuser
# Start development server
python manage.py runserverBackend API available at: http://localhost:8000
cd frontend
# Install dependencies
npm install
# Copy environment file
copy .env.example .env # Windows
cp .env.example .env # Linux/Mac
# Start development server
npm run devFrontend available at: http://localhost:5173
# Django Settings
SECRET_KEY=your-secret-key-here
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
# PostgreSQL Database
DATABASE_ENGINE=django.db.backends.postgresql
DATABASE_NAME=crm_db
DATABASE_USER=crm_user
DATABASE_PASSWORD=your_secure_password
DATABASE_HOST=localhost
DATABASE_PORT=5432
# AWS S3 Configuration
AWS_ACCESS_KEY_ID=your-access-key-id
AWS_SECRET_ACCESS_KEY=your-secret-access-key
AWS_STORAGE_BUCKET_NAME=your-bucket-name
AWS_REGION=us-east-1VITE_API_BASE_URL=http://127.0.0.1:8000/api/v1/| Method | Endpoint | Description |
|---|---|---|
| POST | /api/login/ |
Obtain JWT token |
| POST | /api/token/refresh/ |
Refresh JWT token |
| Method | Endpoint | Description | Permission |
|---|---|---|---|
| GET | /api/v1/companies/ |
List companies | Authenticated |
| POST | /api/v1/companies/ |
Create company | Any role |
| GET | /api/v1/companies/{id}/ |
Retrieve company | Authenticated |
| PUT/PATCH | /api/v1/companies/{id}/ |
Update company | Admin, Manager |
| DELETE | /api/v1/companies/{id}/ |
Delete company | Admin only |
Query Parameters:
page- Page number for paginationpage_size- Items per page (default: 10)search- Search by company name
| Method | Endpoint | Description | Permission |
|---|---|---|---|
| GET | /api/v1/contacts/ |
List contacts | Authenticated |
| POST | /api/v1/contacts/ |
Create contact | Any role |
| GET | /api/v1/contacts/{id}/ |
Retrieve contact | Authenticated |
| PUT/PATCH | /api/v1/contacts/{id}/ |
Update contact | Admin, Manager |
| DELETE | /api/v1/contacts/{id}/ |
Delete contact | Admin only |
Query Parameters:
page- Page numberpage_size- Items per pagesearch- Search by name or email
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/activity-logs/ |
List activity logs (read-only) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/accounts/ |
List users |
| POST | /api/v1/accounts/ |
Create user |
| GET | /api/v1/accounts/{id}/ |
Retrieve user |
| PUT/PATCH | /api/v1/accounts/{id}/ |
Update user |
| DELETE | /api/v1/accounts/{id}/ |
Delete user |
- Login Page (
/login) - JWT authentication - Dashboard (
/) - Organization overview - Companies List (
/companies) - Browse and search companies - Company Detail (
/companies/:id) - View company with nested contacts - Create/Edit Company (
/companies/new,/companies/:id/edit) - Contacts Management - Nested within company detail
- Activity Log (
/activity-log) - Audit trail viewer - User Management - User administration
class Organization(models.Model):
name = CharField(max_length=200)
subscription_plan = CharField(choices=[('BASIC', 'Basic'), ('PRO', 'Pro')])
created_at = DateTimeField(auto_now_add=True)class User(AbstractUser):
organization = ForeignKey(Organization, null=True, blank=True)
role = CharField(choices=[('ADMIN', 'Admin'), ('MANAGER', 'Manager'), ('STAFF', 'Staff')])class Company(models.Model):
name = CharField(max_length=200)
industry = CharField(max_length=150, blank=True)
country = CharField(max_length=100, blank=True)
logo = ImageField(upload_to='company_logos/', null=True, blank=True)
organization = ForeignKey(Organization)
is_deleted = BooleanField(default=False) # Soft delete
created_at = DateTimeField(auto_now_add=True)class Contact(models.Model):
full_name = CharField(max_length=200)
email = EmailField()
phone = CharField(max_length=50, blank=True)
role = CharField(max_length=100, blank=True)
company = ForeignKey(Company, related_name='contacts')
organization = ForeignKey(Organization)
is_deleted = BooleanField(default=False) # Soft delete
created_at = DateTimeField(auto_now_add=True)
class Meta:
unique_together = ['company', 'email'] # Email unique per companyclass ActivityLog(models.Model):
user = ForeignKey(User)
action = CharField(choices=['CREATE', 'UPDATE', 'DELETE'])
model_name = CharField(max_length=200)
object_id = PositiveIntegerField()
organization = ForeignKey(Organization)
timestamp = DateTimeField(auto_now_add=True)- JWT tokens with configurable expiration (1 hour access, 7 days refresh)
- Tokens stored in memory/context (not localStorage for security)
- All protected endpoints require valid JWT tokens
- Role-Based Access Control (RBAC)
- Admin: Full CRUD permissions
- Manager: Create, Read, Update (no delete)
- Staff: Create, Read (limited write access)
- Organization filtering enforced at ViewSet level
- Custom permission classes validate organization membership
- Serializer-level validation prevents cross-organization data manipulation
- AWS S3 with signed URLs (not public access)
- Temporary secure links for file access
- No hardcoded AWS credentials
- IAM-based access control
.envfiles ignored from git.env.exampleprovides template with placeholders- Separate development/production configurations
CRM/
├── backend/
│ ├── .env.example # Environment template
│ ├── manage.py # Django management script
│ ├── requirements.txt # Python dependencies
│ │
│ ├── accounts/ # User authentication & authorization
│ │ ├── models.py # Custom User model
│ │ ├── serializers.py # User serializers
│ │ ├── views.py # User ViewSet
│ │ ├── permissions.py # User permissions
│ │ └── token_serializer.py # Custom JWT token serializer
│ │
│ ├── organizations/ # Organization management
│ │ ├── models.py # Organization model
│ │ └── views.py # Organization views
│ │
│ ├── crm/ # Core CRM functionality
│ │ ├── models.py # Company & Contact models
│ │ ├── serializers.py # CRM serializers with validation
│ │ ├── views.py # Company & Contact ViewSets
│ │ ├── permissions.py # Role-based permissions
│ │ └── mixins.py # Activity log mixin
│ │
│ ├── activity/ # Activity logging
│ │ ├── models.py # ActivityLog model
│ │ └── views.py # ActivityLog ViewSet
│ │
│ └── backend/ # Django project settings
│ ├── settings.py # Configuration
│ ├── urls.py # URL routing (/api/v1/)
│ └── wsgi.py/asgi.py # WSGI/ASGI entry points
│
├── frontend/
│ ├── .env.example # Frontend environment template
│ ├── package.json # Node dependencies
│ ├── vite.config.js # Vite configuration
│ │
│ └── src/
│ ├── App.jsx # Main application component
│ ├── main.jsx # Entry point
│ │
│ ├── components/ # Reusable UI components
│ │ ├── Layout.jsx # Main layout wrapper
│ │ ├── Sidebar.jsx # Navigation sidebar
│ │ ├── Loading.jsx # Loading indicator
│ │ └── Pagination.jsx # Pagination component
│ │
│ ├── context/ # React context providers
│ │ └── AuthContext.jsx # Authentication context
│ │
│ ├── pages/ # Page components
│ │ ├── Login.jsx # Login page
│ │ ├── Dashboard.jsx # Dashboard
│ │ ├── CompanyList.jsx # Companies list
│ │ ├── CompanyDetail.jsx # Company detail with contacts
│ │ ├── CompanyForm.jsx # Create/edit company form
│ │ ├── UserList.jsx # Users list
│ │ ├── UserForm.jsx # Create/edit user form
│ │ └── ActivityLog.jsx # Activity log viewer
│ │
│ ├── routes/ # Route protection
│ │ └── ProtectedRoute.jsx # Auth route wrapper
│ │
│ └── services/ # API layer
│ └── api.js # Centralized API client
│
├── docker-compose.yml # Docker services configuration
├── .gitignore # Git ignore rules
└── README.md # This file
Every CREATE, UPDATE, and DELETE operation automatically generates an audit record:
# Automatic logging via ActivityLogMixin
class CompanyViewSet(ActivityLogMixin, viewsets.ModelViewSet):
# All CRUD operations logged automatically
passLogged Information:
- User who performed the action
- Action type (CREATE/UPDATE/DELETE)
- Model name and object ID
- Timestamp
- Organization
Instead of permanent deletion:
# Mark as deleted
instance.is_deleted = True
instance.save()
# Filter out deleted records
Company.objects.filter(is_deleted=False)Email addresses are unique within each company:
class Meta:
unique_together = ['company', 'email']Validates optional phone field (8-15 digits):
def validate_phone(self, value):
if value:
digits_only = ''.join(filter(str.isdigit, value))
if len(digits_only) < 8 or len(digits_only) > 15:
raise serializers.ValidationError("Phone must be 8-15 digits")
return valueSecure file access without public exposure:
AWS_DEFAULT_ACL = None # No public read
AWS_S3_SECURE_URLS = True # HTTPS only
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'During the development of this multi-tenant CRM system, several advanced technical challenges were encountered and resolved at a senior engineering level:
Challenge: Ensuring strict data isolation between organizations while maintaining query performance as data volume grows. A naive approach could lead to full table scans on every query.
Solution:
- Implemented composite database indexes on
(organization_id, is_deleted)for O(1) tenant lookups - Created a custom
TenantManagerclass that automatically injects organization filters into all ORM queries - Used Django's
select_related()andprefetch_related()strategically to prevent N+1 queries across tenant boundaries - Added database-level constraints to enforce organization foreign keys with
ON DELETE CASCADE
Challenge: Preventing duplicate email creation when multiple users simultaneously create contacts for the same company, and ensuring activity logs remain consistent under concurrent writes.
Solution:
- Used
transaction.atomic()withselect_for_update()to lock rows during critical sections - Implemented database-level unique constraints as the source of truth (not application-level checks)
- Created idempotency keys for CREATE operations to prevent duplicate records from retry logic
- Used optimistic locking patterns with version fields for UPDATE operations
Challenge: Creating a flexible permission system that supports complex business rules beyond simple role checks, such as time-based restrictions, ownership validation, and hierarchical approval workflows.
Solution:
- Implemented a Permission Chain Pattern where multiple permission classes are composed dynamically
- Created policy objects that encapsulate complex business logic separate from views
- Added attribute-based access control (ABAC) alongside role-based access control (RBAC)
- Built permission testing framework to validate all permission combinations
- Dynamically composed permissions based on action type with short-circuit evaluation
Challenge: As the database grows to millions of records across thousands of organizations, ensuring queries remain fast and don't degrade due to excessive JOIN operations or missing indexes.
Solution:
- Implemented database-level Row-Level Security (RLS) policies in PostgreSQL as a defense-in-depth measure
- Used materialized views for frequently accessed aggregated data (e.g., company counts per organization)
- Added query plan analysis to identify slow queries and optimize with targeted indexes
- Implemented connection pooling with PgBouncer for high-concurrency scenarios
- Created composite indexes optimized for tenant-scoped queries with ordering
Challenge: Protecting against OWASP Top 10 vulnerabilities including SQL injection, XSS, CSRF, privilege escalation, and insecure direct object references (IDOR).
Solution:
- Implemented parameterized queries exclusively (Django ORM default, but enforced in raw SQL)
- Added Content Security Policy (CSP) headers and X-Frame-Options
- Created custom middleware to detect and block enumeration attacks
- Implemented rate limiting per organization and per user to prevent abuse
- Added audit logging for failed authentication and authorization attempts
Challenge: Maintaining consistency between Company/Contact creation and ActivityLog recording, especially when operations span multiple database tables or external services (AWS S3).
Solution:
- Implemented the Unit of Work pattern to track changes and commit atomically
- Used two-phase commit protocol for operations involving external services
- Created compensating transactions to rollback changes if any step fails
- Added distributed tracing to track operations across service boundaries
- Wrapped multi-step operations in atomic transactions with proper error handling
Challenge: Implementing soft delete that properly handles cascading deletes (when a company is soft-deleted, its contacts must also be soft-deleted) while maintaining referential integrity and preventing orphaned records.
Solution:
- Created a custom
SoftDeleteManagerthat automatically filters deleted records - Implemented cascade soft delete using Django signals to propagate deletes to related objects
- Added integrity checks to detect and repair orphaned records
- Used database triggers to enforce soft delete constraints at the database level
- Overrode default queryset delete behavior to implement soft delete pattern
Challenge: Ensuring uploaded files (company logos) are isolated by organization in S3, preventing unauthorized access through direct URL manipulation, while optimizing for cost and performance.
Solution:
- Implemented S3 bucket prefix strategy:
s3://bucket/{organization_id}/logos/ - Generated pre-signed URLs with short expiration times (5 minutes) for temporary access
- Added S3 bucket policies to deny all public access and require signed requests
- Implemented server-side encryption (SSE-S3) for data at rest
- Created cleanup jobs to remove orphaned files when organizations are deleted
- Built custom storage backend class to handle organization-based file isolation
Challenge: Activity logging must not degrade CRUD operation performance, even with millions of log entries. The logging system must be asynchronous, non-blocking, and queryable.
Solution:
- Implemented async task queue (Celery + Redis) for non-blocking log writes
- Created partitioned tables for ActivityLog (partitioned by organization and month)
- Added log retention policies with automatic archival to cold storage (S3 Glacier)
- Used bulk insert operations for batch operations to reduce database round trips
- Configured exponential backoff retry logic for failed log writes
Challenge: Deploying schema changes (adding organization foreign keys, indexes) to production without downtime or data loss, while maintaining backward compatibility during rolling deployments.
Solution:
- Followed the "Expand and Contract" pattern for schema migrations
- Made all migrations reversible with proper
reverse_code()methods - Used nullable foreign keys initially, then backfilled data, then added constraints
- Created data migration scripts with batch processing to avoid long locks
- Split migrations into multiple steps: add nullable field → backfill data → make non-nullable → add indexes
- Backend: Django ORM, DRF serializers, custom permissions
- Frontend: ESLint, React best practices, component architecture
Access browsable API docs at: http://localhost:8000/api/v1/
Django admin available at: http://localhost:8000/admin/
This project is created for the Associate Full Stack Developer Technical Examination.
GitHub Repository: github.com/Inkithai/CRM
This is an examination project. For production use, consider adding:
- Comprehensive test suites
- CI/CD pipelines
- Advanced RBAC features
- Real-time notifications (WebSockets)
- Email verification workflows
- Rate limiting
- Advanced search and reporting features
For questions or issues, please open an issue on GitHub.
Built with ❤️ using Django, React, and PostgreSQL