Glossary¶
Terms used throughout this documentation that are either specific to BottleCRM's own model, or that sound generic but resolve to a particular model or field in this codebase. Each entry links to the page that covers the concept in full; this page is the short definition, not the whole story.
Terms¶
Organization¶
Org (backend/common/models.py, db table organization), the tenant boundary. Every org-scoped
record carries an org foreign key to this model, either declared directly or inherited from
BaseOrgModel, and Row-Level Security enforces that a request in one organization's context never
returns another organization's rows. Carries a default_currency/default_country locale pair, the
organization API key, and, once a vertical pack has been applied,
vertical and terminology fields that are descriptive only: the model's own comment states
they are never used in a permission check or a queryset filter.
Profile¶
Profile (backend/common/models.py), a user's membership in one organization: a (user, org)
pair, unique together, carrying that membership's role (ADMIN/USER, from ROLES),
is_organization_admin, has_sales_access/has_marketing_access, and is_active. One User can
hold a separate Profile, with a separate role, in each organization they belong to.
request.profile, set by the GetProfileAndOrg middleware from the JWT on every request, is what
every permission check and org filter in this codebase is written against, never request.user
directly. See Permissions and roles.
Tenant¶
Not a distinct model or field in this codebase; the general term this documentation (and the wider industry) uses for "one organization's data, isolated from every other organization's." In BottleCRM, an organization is the tenant boundary. See Multi-tenancy and RLS.
RLS context¶
app.current_org: the PostgreSQL session variable Row-Level Security policies read to decide
which rows a query is allowed to see. CONTEXT_VARIABLE in backend/common/rls/__init__.py names
it; the RequireOrgContext middleware (backend/common/middleware/rls_context.py) sets it once per
request, via SELECT set_config('app.current_org', <org_id>, false), and clears it afterward. The
org id it sets comes from request.org, one hop removed from request.profile.org: the earlier
GetProfileAndOrg middleware (backend/common/middleware/get_company.py) is what actually resolves
the org, from the JWT, a personal access token, or an organization API key, depending on which
authentication path the request took, and assigns it to request.org directly (as well as setting
request.profile); RequireOrgContext only reads request.org, not request.profile.org, when
deciding what to set. A request with no context set sees zero rows from every RLS-protected table.
The fail-safe every isolation policy's NULLIF(current_setting(...), '') clause is built around.
See Multi-tenancy and RLS.
Vertical pack¶
A small, declarative JSON manifest (backend/packs/*.json, validated by
backend/common/packs/schema.py) that shapes an already-existing organization toward one
industry: a starter pipeline (with stages) for leads, cases, and tasks; custom field definitions;
tags; products; terminology overrides (for example relabeling "Lead" as "Enquiry"); and a short list
of named sample records that reference each other within the manifest. Applying a pack is a
one-time, idempotent action, distinct from seed_data. See
Management commands, which generates a large randomized
dataset instead of a curated one. See
Demo data and packs.
Portal token¶
PortalAccessToken (backend/common/models.py): a token minted for an anonymous, unauthenticated
visitor (someone following a shared invoice/estimate link, or answering a CSAT survey) to reach
exactly one record without signing in. Deliberately absent from ORG_SCOPED_TABLES and carrying no
RLS policy of its own, because a portal request has no request.profile to derive an org context
from in the first place: the table instead maps sha256(url_token) to an org_id directly, so the
public view can hash the token it was handed, resolve the org, set the RLS context, and only then
query the actual resource. See
Multi-tenancy and RLS: Portal tokens.
Not to be confused with the portal session token below, which is a different mechanism that unfortunately shares the word.
Portal session token¶
The credential a customer holds after signing in to the customer portal:
a 24 hour JWT carrying typ: "portal", org_id and contact_id, minted by mint_portal_token
(backend/common/portal_auth.py). Its principal is a Contact, not a User, and it is refused
on every path outside /api/portal/.
The distinction from the portal token above is worth holding on to: that one is
held (the URL is the credential, anyone with the link is in, and it reaches one record), this one
is earned (the customer proves control of an email address first, and it reaches every case they
are named on). A separate PortalLoginToken row carries the emailed six digit code that is
exchanged for it.
Portal contact¶
A Contact who has signed in to the customer portal. Not a user of your CRM: no User row, no
Profile, no role, and no org membership. Comments they write carry commented_by_contact and
leave commented_by null, which is how every reader tells a customer's reply from a colleague's.
Personal access token¶
bcrm_pat_… (PersonalAccessToken, backend/common/models.py): a token a signed-in user creates
for themselves (POST /api/profile/tokens/) to authenticate scripts, AI agents, or other
programmatic access without a browser session. It authenticates as the profile that created it and
inherits that profile's role and org in full; its scopes field is stored but not enforced.
Deactivating the owning profile invalidates the token immediately; resolve_valid_pat checks
profile.is_active on every use, with no separate revocation step required. See
Tokens and API keys: Personal access tokens.
Organization API key¶
Org.api_key: a single key per organization, not per user, managed through GET/POST
/api/org/api-key/, admin-only in both directions and only from an interactive session. A request
bearing it in the Token header authenticates as the organization's first active ADMIN profile
rather than any specific person. It is read-only and cannot reach a credential endpoint, and
DJANGO_ORG_API_KEY_AUTH=false disables it outright, but it still reads every record in the org and
cannot be revoked per integration, so a personal access token (which authenticates as one specific
individual, expires, and carries scopes) is the better choice for anything new. See
Tokens and API keys: Organization API keys.
Stage versus status¶
Two different kinds of field that sound alike but aren't. status (Lead.status, Case.status,
Task.status) is a plain CharField over a fixed set of choices, three separate tuples, not one
shared definition: LEAD_STATUS and STATUS_CHOICE, defined in backend/common/utils.py, back
Lead.status and Case.status respectively, while Task.STATUS_CHOICES is defined inline on the
Task model itself in backend/tasks/models.py. Each one is still fixed and identical across every
organization. That part of the "status vs. stage" distinction holds for all three. stage, on those
same three
models, is a nullable foreign key to a per-org LeadStage/CaseStage/TaskStage row, itself
belonging to a LeadPipeline/CasePipeline/TaskPipeline (see Pipeline). Each
organization can define its own Kanban columns, and a stage's maps_to_status field is what keeps
the fixed status in sync when a record moves between stages. Opportunity.stage, despite sharing
the field name, is the fixed kind: a plain CharField over common.utils.STAGES (PROSPECTING
… CLOSED_LOST), the same six values for every organization, with no configurable pipeline behind
it and no separate status field at all. See
Data model: Conventions for the full explanation this
entry summarizes.
Pipeline¶
LeadPipeline, CasePipeline, TaskPipeline: an org-owned, named container of ordered Stage
rows for one entity type (see Stage versus status). An organization can
define more than one pipeline for the same entity type. Each model's own docstring gives an
example set of names (Inbound/Outbound/Enterprise for leads, Support/Engineering/Billing for cases,
Development/Support/Marketing for tasks), and at most one per organization may be marked
is_default, enforced by a UniqueConstraint on org scoped to is_default=True; nothing requires
that one exist. The field's own help_text says a default pipeline is "where new [records] without
explicit pipeline go", but that's a statement of intent, not a description of running code: nothing
in this codebase currently reads is_default to route a new Lead, Case, or Task onto a
pipeline. A record's stage foreign key is simply NULL until something sets it explicitly, which
its own help_text calls "use status-based kanban": the fixed status field, not a pipeline stage,
is what a newly created record actually falls back to. Opportunity has no equivalent pipeline
model: its stage list is the fixed set above, not org-configurable.