Skip to content

Management commands

Every command below is BottleCRM's own. The entire common/management/commands/ package holds exactly these five files. Run uv run python manage.py help from backend/ and everything outside the [common] section comes from Django itself or an installed third-party package (django, auth, contenttypes, sessions, staticfiles, django_ses, drf_spectacular, rest_framework, token_blacklist), none of it is project-specific, so it isn't repeated here. Run any command from backend/ with uv run python manage.py <command> (or, against the Docker stack, docker compose exec backend python manage.py <command>).

manage_rls

Read-only diagnostics for Row-Level Security. None of its three flags modify the database. RLS policies are enabled and disabled by Django migrations, calling get_enable_policy_sql(), never by this command (the module's own docstring says so explicitly). It checks the tables listed in ORG_SCOPED_TABLES (common/rls/__init__.py), which as of this writing has 61 entries. The repository's CLAUDE.md still says 24; that figure is out of date, and the count is deliberately not repeated anywhere outside ORG_SCOPED_TABLES itself so it cannot drift again.

uv run python manage.py manage_rls --status
uv run python manage.py manage_rls --test
uv run python manage.py manage_rls --verify-user
Flag What it does
--status For every table in ORG_SCOPED_TABLES, queries pg_class.relrowsecurity/relforcerowsecurity and prints ENABLED (forced), disabled, or TABLE NOT FOUND, plus whether the connected role is a superuser. This is also the default, running manage_rls with no flags at all runs --status.
--test Picks two organizations (preferring ones that actually have Lead rows), sets the RLS context to each in turn and counts visible leads, then sets an empty context and counts again. Zero rows with no context and nonzero with a real context means isolation is working; nonzero rows with no context is a warning, not a failure, since some policies deliberately allow an empty context.
--verify-user Queries pg_user for usesuper/usecreatedb on the connected role and prints an explicit WARNING: Superusers bypass RLS! with remediation SQL if the role is a superuser.

This only covers what the command itself does. For why a superuser role defeats every policy silently, how to create a correctly-restricted role, and what each flag's output actually proves, see PostgreSQL and RLS.

seed_data

Generates a large, randomized dataset, accounts, contacts, leads, opportunities, cases, tasks, invoices and more, for one or more organizations, entirely from curated fictional name/company pools and reserved demo domains (example.com/.example) so nothing it creates resembles a real person or business. The first organization created (--orgs index 0) is always named MicroPyramid, specifically so local-dev workflows have a stable org name to sign in against with devlogin --org MicroPyramid (see below). For the default, single-org run, re-running the command with the same --email reuses that existing user, org, and profile rather than creating duplicates; create_org does an Org.objects.get_or_create(name="MicroPyramid", ...). That reuse guarantee is specific to index 0: any organization beyond the first (--orgs 2 or higher) gets a name drawn randomly from a company-name pool each run, so re-running with --orgs 2 typically accumulates additional organizations rather than reusing the second one from the previous run.

uv run python manage.py seed_data --email you@example.com
uv run python manage.py seed_data --email you@example.com --orgs 2 --leads 100 --seed 42
uv run python manage.py seed_data --email you@example.com --clear --no-input
Flag Default What it does
--email aswin.1231@gmail.com Email of the admin user to create (or reuse) as the seeded org's admin profile.
--orgs 1 Number of organizations to create.
--users-per-org 3 Additional non-admin users created per org.
--leads 20 Leads per org.
--accounts 10 Accounts per org.
--contacts 15 Contacts per org.
--opportunities 10 Opportunities per org.
--cases 5 Cases per org.
--tasks 10 Tasks per org.
--goals 8 Sales goals per org.
--teams 2 Teams per org.
--tags 5 Tags per org.
--products 20 Products per org.
--invoices 50 Invoices per org.
--estimates 15 Estimates per org.
--recurring-invoices 5 Recurring invoices per org.
--invoice-templates 3 Invoice templates per org.
--currency USD Default org currency; restricted to CURRENCY_CODES choices.
--country US Default org country code; also selects the Faker locale used for generated names/addresses (falls back to en_US for an unmapped code).
--seed none Random seed, for a reproducible run.
--password testpass123 Password set on newly created users.
--clear off (flag) Deletes existing CRM data before seeding, not users, orgs, or profiles. Prompts for confirmation unless --no-input is also given. This is a separate, broader operation from a vertical pack's own sample-data clearing (see Demo data and packs) and isn't scoped to pack-created records specifically.
--no-input off (flag) Skips the --clear confirmation prompt.

See Demo data and packs for the narrative walkthrough, including what a full seeded org looks like and how vertical packs layer on top of it.

devlogin

Mints a JWT access/refresh pair for an existing user directly from the command line, no OAuth provider, no outbound email, no browser round-trip. Refuses to run unless settings.DEBUG is True, raising a CommandError as the very first thing handle() does. That means it prints an error and exits non-zero in any environment where DEBUG=False, not that it silently does nothing.

uv run python manage.py devlogin <email> --org <name-or-uuid>
uv run python manage.py devlogin <email> --create
Argument What it does
email (positional, required) The user to mint a token for.
--org Optional org name or UUID. Resolves id first, then name: it tries Org.objects.get(id=org_arg) first, and only falls back to matching on Org.name if that lookup fails (not found, or org_arg isn't a valid UUID at all). More than one org sharing that name is refused outright rather than guessed at, pass an id instead. The target user must already have an active Profile in the resolved org. This command does not create one, and raises a CommandError naming the missing user/org pair if none exists.
--create Creates the user (with a random, discarded password) if no user with that email exists yet. This only creates the User row. It has no effect on the Profile requirement above; a freshly created user still needs a Profile in the target org before --org will succeed for them.

On success it prints an access token and a refresh token, the org's UUID if --org was given (a token minted this way already carries the org_id claim, so no follow-up org-switch call is needed), and a localStorage.setItem(...) snippet for a browser console. That snippet alone does not sign you into the SvelteKit app itself; see Using the tokens in the browser for the cookies hooks.server.js actually checks. See First sign-in: Local development for the full walkthrough, including what happens with no --org at all.

create_default_admin

Creates a Django superuser, but only if none already exists; User.objects.filter(is_superuser= True).exists() short-circuits the whole command to a no-op if one is found, so it's safe to run on every startup. It takes no arguments at all (no add_arguments override). It reads ADMIN_EMAIL and ADMIN_PASSWORD directly with os.environ.get, independently of crm/settings.py (ADMIN_EMAIL happens to share a name and a default with the copy settings.py also reads; ADMIN_PASSWORD is read nowhere else). If ADMIN_PASSWORD is unset or empty, the command falls back to the literal password admin and prints a warning. It does not refuse to create the account. This command runs automatically on every Docker container start (docker/backend/entrypoint.sh, immediately after migrate), so in the Docker quick start it never needs to be invoked directly, and an unset ADMIN_PASSWORD in a real deployment means the bootstrap superuser's password really is admin until it's changed by hand.

uv run python manage.py create_default_admin

audit_org_fields

A one-time Phase 1 multi-tenancy hardening tool, not a general or self-updating auditor: it checks a hardcoded pair of Python lists inside the command itself, models_with_nullable_org (currently just common.Address and common.Tags) and models_with_required_org (thirteen named models), not every org-scoped model that exists today. A model added to the codebase since this command was last edited is invisible to it either way.

uv run python manage.py audit_org_fields
uv run python manage.py audit_org_fields --fix
uv run python manage.py audit_org_fields --verbose

The audit always runs; there is no way to suppress it short of not invoking the command. A --check flag used to be accepted by the parser and never read in handle(), so passing it or omitting it produced identical output; it has been removed rather than left as a flag that means nothing.

Flag What it does
--fix For common.Address rows with a null org, attempts to backfill it from a related Profile. For common.Tags, --fix does not fix anything. It prints a message asking for manual review instead. For any other model that turns up with a null org, it also just prints "Automatic fix not implemented."
--verbose Two effects, one per pass: in the nullable-org pass it prints each model's field configuration (null, blank, related_query_name); in the required-org pass it's what makes a "Model not found" warning print at all. Without --verbose, a LookupError there is swallowed silently.

Output uses ✅/❌/⚠️ markers per model and ends with a summary and a suggested next-steps block (create a migration, run makemigrations/migrate): it never modifies the schema itself, only data, and only when --fix is passed.