| .. | ||
| accounts | ||
| cases | ||
| common | ||
| contacts | ||
| crm | ||
| invoices | ||
| leads | ||
| opportunity | ||
| orders | ||
| static | ||
| tasks | ||
| templates | ||
| .env.example | ||
| .python-version | ||
| conftest.py | ||
| manage.py | ||
| pyproject.toml | ||
| pytest.ini | ||
| README.md | ||
| ruff.toml | ||
| seed.md | ||
| uv.lock | ||
BottleCRM Backend - Django REST API
The backend for BottleCRM, a multi-tenant CRM platform built with Django REST Framework.
Tech Stack
- Django 4.2.1 - Web framework
- Django REST Framework 3.14.0 - API toolkit
- PostgreSQL - Database (psycopg2-binary 2.9.11)
- Celery 5.5.3 - Async task queue
- Redis 4.6.0 - Message broker for Celery
- djangorestframework-simplejwt 5.2.2 - JWT authentication
- drf-spectacular 0.26.2 - OpenAPI/Swagger documentation
- django-ses 3.5.0 - AWS SES email backend
- Sentry SDK 1.24.0 - Error tracking
Django Apps
| App | Description |
|---|---|
common |
User, Organization, Profile, Comments, Attachments, Document models |
accounts |
Customer account management |
leads |
Lead tracking and conversion |
contacts |
Contact management |
opportunity |
Sales pipeline and deal tracking |
cases |
Customer support tickets |
tasks |
Task management |
invoices |
Invoicing system |
events |
Calendar events |
teams |
Team management |
emails |
Email handling |
planner |
Planner events |
boards |
Kanban board system |
marketing |
Newsletter and contact forms |
Prerequisites
- Python 3.10+ (uv installs a matching Python automatically if needed)
- PostgreSQL
- Redis (for Celery)
- uv — Python package & venv manager (replaces pip + virtualenv)
Installation
1. Install uv
# Linux / macOS
curl -LsSf https://astral.sh/uv/install.sh | sh
# Or via Homebrew
brew install uv
2. Install Python dependencies
uv sync reads pyproject.toml + uv.lock, picks the Python version from .python-version, and creates .venv/ with everything installed.
cd backend
uv sync
Run any backend command with
uv run <cmd>(e.g.uv run python manage.py migrate). uv resolves binaries from.venv/bin/automatically — no manualsource .venv/bin/activateneeded (though that still works if you prefer it).
3. Install PDF generation system dependencies
Invoice PDF generation uses WeasyPrint (a runtime dep). It requires system libraries that must be installed separately:
Ubuntu/Debian:
sudo apt-get install -y \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libcairo2 \
libgdk-pixbuf2.0-0 \
libffi-dev \
shared-mime-info
macOS:
brew install pango cairo libffi gdk-pixbuf
Fedora/CentOS:
sudo dnf install -y \
pango \
cairo \
gdk-pixbuf2 \
libffi-devel
Windows: Follow the WeasyPrint Windows installation guide.
Note
: If you skip this step, the CRM will work but PDF download for invoices will show "PDF generation unavailable".
4. Configure environment variables
Create a .env file in the backend/ directory:
# Django
SECRET_KEY=your-secret-key-here
ENV_TYPE=dev
# Database
DBNAME=bottlecrm
DBUSER=postgres
DBPASSWORD=root
DBHOST=localhost
DBPORT=5432
# Email
DEFAULT_FROM_EMAIL=noreply@bottlecrm.com
ADMIN_EMAIL=admin@bottlecrm.com
# Celery
CELERY_BROKER_URL=redis://localhost:6379/0
CELERY_RESULT_BACKEND=redis://localhost:6379/0
# Domain
DOMAIN_NAME=http://localhost:8000
SWAGGER_ROOT_URL=http://localhost:8000
5. Set up database
# Create PostgreSQL database
sudo -u postgres psql
CREATE DATABASE bottlecrm WITH OWNER = postgres;
ALTER USER postgres WITH PASSWORD 'root';
\q
# Run migrations
uv run python manage.py migrate
# Create superuser (optional)
uv run python manage.py createsuperuser
6. Run the development server
uv run python manage.py runserver
The API will be available at http://localhost:8000
Running Celery
For background tasks (emails, notifications), run the Celery worker:
uv run celery -A crm worker --loglevel=INFO
API Documentation
- Swagger UI: http://localhost:8000/swagger-ui/
- ReDoc: http://localhost:8000/api/schema/redoc/
- Django Admin: http://localhost:8000/admin/
Generating Schema
To generate the OpenAPI schema file:
python manage.py spectacular --file openapi.yml
Architecture
Multi-Tenancy
Every request operates within an organization context:
- Organization (Org): Top-level tenant container
- Users: Regular members with USER role
- Admins: Organization administrators with ADMIN role
- Super Admin: Users with @micropyramid.com email domain have platform-wide access
Authentication
JWT-based authentication:
Authorization: Bearer <token>
- Organization ID is embedded in the JWT token (not sent as header)
- Access token lifetime: 1 day
- Refresh token lifetime: 365 days
Middleware
The middleware chain provides security:
-
GetProfileAndOrg(common.middleware.get_company):- Extracts org_id from JWT token claims (not headers - prevents spoofing)
- Validates user has active membership in the organization
- Sets
request.profileandrequest.org
-
RequireOrgContext(common.middleware.rls_context):- Sets PostgreSQL session variable
app.current_orgfor RLS - Resets context after each request
- Sets PostgreSQL session variable
Row-Level Security (RLS)
PostgreSQL RLS provides database-level tenant isolation as defense-in-depth.
How It Works
- Middleware sets context:
SET app.current_org = '<org_id>' - RLS policies filter queries: Only rows matching
org_idare visible - Fail-safe design: Empty context returns zero rows (NULLIF pattern)
Protected Tables (24 total)
| Category | Tables |
|---|---|
| Core Business | lead, accounts, contacts, opportunity, case, task, invoice |
| Supporting | comment, attachments, document, teams, activity, tags, address, solution |
| Boards | board, board_column, board_task, board_member |
| Other | apiSettings, account_email, emailLogs, invoice_history, security_audit_log |
Configuration
RLS is configured in common/rls/__init__.py:
from common.rls import RLS_CONFIG, get_enable_policy_sql
# List of protected tables
tables = RLS_CONFIG['tables']
# Enable RLS on a table
cursor.execute(get_enable_policy_sql('my_table'))
Management Commands
# Check RLS status on all tables
python manage.py manage_rls --status
# Verify database user is non-superuser (required for RLS)
python manage.py manage_rls --verify-user
# Test RLS isolation between organizations
python manage.py manage_rls --test
# Enable RLS on all configured tables
python manage.py manage_rls --enable
# Disable RLS (for debugging only)
python manage.py manage_rls --disable
Critical: Database User Setup
PostgreSQL superusers bypass ALL RLS policies. You must use a non-superuser:
-- Create application user
CREATE USER crm_app WITH PASSWORD 'your_secure_password';
-- Grant permissions
GRANT CONNECT ON DATABASE bottlecrm TO crm_app;
GRANT USAGE ON SCHEMA public TO crm_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO crm_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO crm_app;
-- Future tables inherit permissions
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO crm_app;
Update .env:
DBUSER=crm_app
DBPASSWORD=your_secure_password
Celery Tasks & RLS
Background tasks don't go through middleware, so set RLS context manually:
from common.tasks import set_rls_context
@app.task
def my_background_task(data_id, org_id):
set_rls_context(org_id) # Required!
obj = MyModel.objects.get(id=data_id)
# ... process
Adding RLS to New Tables
- Add table name to
ORG_SCOPED_TABLESincommon/rls/__init__.py - Create migration using
get_enable_policy_sql() - Ensure model has
org = models.ForeignKey(Org, ...)
BaseModel Pattern
All models inherit from BaseModel (common.base.BaseModel):
- UUID primary keys (not integer IDs)
- Automatic timestamps:
created_at,updated_at - Audit trail:
created_by,updated_by - Organization isolation:
org = models.ForeignKey(Org)
API Endpoint Pattern
GET/POST /api/<module>/ # List/Create
GET/PUT/DELETE /api/<module>/<pk>/ # Detail/Update/Delete
GET/POST /api/<module>/comment/<pk>/ # Comments
GET/POST /api/<module>/attachment/<pk>/ # Attachments
Project Structure
backend/
├── manage.py
├── pyproject.toml # Python deps + project metadata (uv-managed)
├── uv.lock # Pinned, reproducible dependency tree
├── .python-version # Python version pin (uv reads this on `uv sync`)
├── crm/ # Django project settings
│ ├── settings.py
│ ├── urls.py
│ ├── celery.py
│ └── wsgi.py
├── common/ # Core models and utilities
│ ├── models.py # User, Org, Profile, etc.
│ ├── base.py # BaseModel
│ ├── middleware/
│ └── tasks.py # Celery tasks
├── accounts/
├── leads/
├── contacts/
├── opportunity/
├── cases/
├── tasks/
├── invoices/
├── events/
├── teams/
├── emails/
├── planner/
├── boards/
├── marketing/
├── templates/
└── static/
Development
Code Quality
# Format code
uv run black .
# Sort imports
uv run isort .
# Run tests
uv run pytest
Managing Dependencies
# Add a runtime dependency (updates pyproject.toml + uv.lock)
uv add <package>
# Add a dev-only dependency (e.g. test or lint tool)
uv add --group dev <package>
# Remove a dependency
uv remove <package>
# Refresh the lockfile
uv lock --upgrade
Creating a New App
-
Create the app:
uv run python manage.py startapp myapp -
Add to
INSTALLED_APPSincrm/settings.py -
Create models inheriting from
BaseModel:from common.base import BaseModel from common.models import Org class MyModel(BaseModel): org = models.ForeignKey(Org, on_delete=models.CASCADE) # ... other fields -
Always filter queries by organization:
queryset = MyModel.objects.filter(org=request.profile.org) -
Run migrations:
uv run python manage.py makemigrations uv run python manage.py migrate
Environment Variables Reference
| Variable | Description |
|---|---|
SECRET_KEY |
Django secret key |
ENV_TYPE |
Environment type (dev or prod) |
DBNAME |
PostgreSQL database name |
DBUSER |
PostgreSQL username |
DBPASSWORD |
PostgreSQL password |
DBHOST |
PostgreSQL host |
DBPORT |
PostgreSQL port |
DEFAULT_FROM_EMAIL |
Default sender email |
ADMIN_EMAIL |
Admin notification email |
CELERY_BROKER_URL |
Redis URL for Celery broker |
CELERY_RESULT_BACKEND |
Redis URL for Celery results |
DOMAIN_NAME |
Application domain |
SWAGGER_ROOT_URL |
Swagger documentation root URL |
Troubleshooting
Database Connection Issues
# Check PostgreSQL is running
sudo systemctl status postgresql
# Verify database exists
sudo -u postgres psql -l
Migration Issues
# Show migration status
uv run python manage.py showmigrations
# Reset migrations (development only)
uv run python manage.py migrate --fake <app> zero
Celery Not Processing Tasks
# Check Redis is running
redis-cli ping
# Check Celery worker logs
uv run celery -A crm worker --loglevel=DEBUG
License
MIT License - see LICENSE for details.