Key concepts
A short tour of the moving parts in BottleCRM.
Organizations (tenants)
Everything in BottleCRM is scoped to an organization. A user belongs to one or more organizations through a Profile. The currently active organization is encoded in the JWT, and the backend enforces tenant isolation in two layers:
- Application layer:
Model.objects.filter(org=request.profile.org)on every queryset. - Database layer: PostgreSQL row-level security policies that read
app.current_orgfrom a session variable set per request.
The database user the app connects as is intentionally not a superuser, so even a buggy query cannot return another tenant's rows. See Postgres + RLS.
Profiles and roles
A Profile joins a user to an organization with a role: ADMIN or USER. Permissions are derived server-side from the profile, never trust an is_admin boolean sent from the client.
From django-crm 1.11.0 a Django superuser counts as an admin in every organization they belong to, whatever the role on their profile, on every endpoint and in both clients. They still need a profile in that org to reach it at all. Superuser status is granted with createsuperuser or the Django admin, never through the API.
A user's login email is global to their account, so it cannot be changed through the user API (PUT or PATCH /api/user/<id>/), by an admin editing a colleague or by the user themselves. Sending the stored address back unchanged is accepted; any other address is a 400. The profile picture follows the same logic, because it too belongs to the account rather than to one org: an admin can no longer change another member's picture, only their own.
Entities
The core CRM entities and what they're for:
| Entity | Purpose |
|---|---|
| Lead | Top of funnel, an unqualified person or company. |
| Account | A qualified customer (or prospect) organization. |
| Contact | A person at an account. |
| Opportunity | A potential deal attached to an account. |
| Task | Anything someone needs to do, optionally linked to a record. |
| Case (Ticket) | A customer support issue. |
| Invoice / Estimate / RecurringInvoice | Billing artifacts. |
Each of these supports per-organization custom fields. See Custom fields.
Authentication
The web app and mobile app authenticate with JWT access + refresh tokens. Tokens are minted by:
- Google OAuth (
POST /api/auth/google/) - Magic link email (
POST /api/auth/magic-link/request/→POST /api/auth/magic-link/verify/) - Dev login (
manage.py devlogin, local only)
The SvelteKit shell stores tokens in httpOnly cookies (jwt_access, jwt_refresh, org); the mobile and API clients hold them in their respective secure stores.
Background work
Long-running operations (sending invoice emails, delivering and retrying webhooks) are dispatched to Celery. Critically, Celery tasks must re-establish the tenant context with set_rls_context(org_id) from common.tasks before touching the ORM. There is no middleware in the worker.