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, SALES MANAGER, SALES REP, or USER. Permissions are derived server-side from the profile — never trust an is_admin boolean sent from the client.
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, importing CSVs) 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.