Documentation menu

DocsGet started

Editions and capabilities

From docs/CAPABILITIES.md in the repository.

On this page

D7 in DEVELOPMENT_PLAN.md: an organization has one edition (community, cloud or enterprise). Each edition maps to a set of capability names in apps/api/core/capabilities.py. Features check a capability name and never the edition; tests/test_capabilities.py fails if code outside core/capabilities.py, core/limits.py (the per-edition limit defaults) and core/models.py compares an edition.

Mapping

Capability Community Cloud Enterprise Gate in the API today
catalog (applications, components, integrations, people, teams, vendors) yes yes yes none (always on)
relationships (relationships, dependencies, change assessments) yes yes yes none
reviews (record reviews, staleness) yes yes yes none
renewals yes yes yes none
notifications.digest (daily digest, Needs attention snoozes) yes yes yes none
discovery.entra yes yes yes POST /connectors with source=entra, entra_licenses or (Phase 11) entra_shadow_it, entra_coverage or entra_licence_usage; GET /discovery/shadow-it, GET /reports/sso-coverage, GET /reports/licence-waste and their export.csv, the license waste drill-down GET /reports/licence-waste/{sku_id} (Admin) and its CSV, GET /license-pools/{id}/licence-waste and GET /contracts/{id}/licence-waste (policy(capability=discovery.entra); the last two also need contracts, and license waste prices only with contracts); the application detail’s sso_coverage; the data-quality rules sso_missing, mfa_missing, scim_missing; the offboarding footprint’s licenses section. License waste is community: the plan lists it without an (E) mark, and it reads the same Entra tenant as the other Entra sources
imports (CSV import and export) yes yes yes none
api.tokens yes yes yes none
audit.log (in-app audit log, its CSV export) yes yes yes GET /audit-events/export.csv (required_capability; Admin)
sso.oidc (OpenID Connect single sign-on: Entra ID, Okta, Google Workspace, any OIDC provider; Phase 8) yes yes yes every /sso/settings... route (policy(capability=sso.oidc)); without it auth/options offers no SSO and start refuses (core/sso.effective)
ai_agents (AI agents, AI providers and models, the models-in-use report; Phase 2) yes yes yes every /ai-agents, /ai-providers and /ai-models route (required_capability), so an edition without it gets the capability problem; every edition has it today; global search leaves agents out without it (403 when kinds names ai_agent)
custom_fields (custom field definitions and values; Phase 2) yes yes yes every /custom-fields route (required_capability); values on records go through the records’ own routes; global search ignores custom field values without it
attachments (files and links on records and vendors; Phase 2) yes yes yes every /<records>/{id}/attachments, /vendors/{id}/attachments and /attachments/{id} route (required_capability); an AI agent’s also need ai_agents; global search leaves attachments out without it (403 when kinds names attachment)
data_quality (data-quality rules, exceptions, the completeness score and the Data quality queue; Phase 2) yes yes yes every /data-quality/... route (required_capability); without it no rule is on, so records’ quality lists no failing check and Needs attention leaves the Data quality queue empty. Rules inside it are gated by their own capability: the AI agent checks by ai_agents, the compliance checks by compliance_profile and the profile being on
contracts (contracts, contract lines and license pools; Phase 4) yes yes yes every /contracts... and /license-pools... route (required_capability), contract attachments and their audit routes; without it global search leaves contracts out (403 when kinds names contract), a record’s Map leaves contracts and license pools out, attachments on contracts are left out of search, the data-quality check “Not on a contract” is unavailable, a record’s History leaves out its contract-line events, custom fields can’t be newly applied to contract or license_pool (fields that already apply keep them) and contract and license pool date fields are no reminder source
webhooks (signed event webhooks: endpoints, subscriptions, the delivery log; Phase 7) yes yes yes every /webhooks... and /webhook-deliveries... route (policy(capability=webhooks)); without it no event is queued (notify/webhooks.deliver). Phase 5 webhook destinations named by notification rules (/notification-settings/webhook-destinations) aren’t gated
vendor_risk (the vendor risk file: profile, risk documents, their expiry reminders, the risk overview and its gaps; Phase 11) yes yes yes every /vendors/{id}/risk..., /vendor-risk-documents... and /vendors/risk-overview... route (required_capability / policy(capability=vendor_risk)); without it the vendor detail’s risk is null and there are no vendor_risk reminders, Vendor risk queue rows or vendor data-quality checks
org.export (export all of the organization’s data as a zip; Phase 10) yes yes yes POST /organization/exports (Admin). Listing and downloading go by each export’s purpose: an Admin’s own exports need org.export; the final export before deletion needs org.export or org.self_delete (the deletion banner links it); nightly backups need org.export or org.tenant_backups; an export the operator made on the organization’s request (operator) is always listed and downloadable by its Admins. Any other is a 404
eol (end-of-life tracking of on-prem software versions via endoflife.date; Phase 11) yes yes yes every /end-of-life... route and each record’s /<records>/{id}/end-of-life and instance .../end-of-life route (policy(capability=eol)); without it a record’s end_of_life is null, Needs attention’s End of life queue is empty and the data-quality check “End-of-life cycle unknown” is unavailable. Community: the data is public (endoflife.date, MIT) and the feature is a natural part of a system of record for on-prem software
catalog_code (catalog-as-code: the catalog as YAML files in git; Phase 11) yes yes yes GET /catalog-code/export.zip, POST /catalog-code/plan and POST /catalog-code/apply (policy(capability=catalog_code), Admin) and the catalog_export and catalog_apply commands. Community because it is how a self-hosted team keeps its catalog under review in git, and it writes only through the same services and limits as the app. Inside it, each kind keeps its own gate: AI agents, providers and models need ai_agents, custom field definitions and values custom_fields, relationships and their types relationships, attachment metadata attachments, and the compliance fields the profile being on; a file of a kind the organization lacks is that file’s error, and the export leaves it out
data_flows (the data-flow map: where classified data goes, risky flows, its graph and CSV; Phase 11) yes yes yes GET /reports/data-flows, /reports/data-flows/graph and /reports/data-flows/export.csv (policy(capability=data_flows), any role); without it Needs attention’s Data flows queue is empty and integrations aren’t checked by data quality (the rules “Data flow not classified” and “No data categories” are unavailable). An integration’s data_categories are edited on the integration in every edition, like its classification. Community: it reads data every edition records and the plan gives it no (E) mark
ticket_links (Jira, ServiceNow and Halo ticket links on assessments and renewals, their status refresh and opt-in ticket creation; Phase 11) yes yes yes every /assessments/{id}/ticket-links..., /renewals/{id}/ticket-links..., /ticket-links/parse and /ticket-connections... route (policy(capability=ticket_links)); without it the assessment and renewal details’ ticket_links are empty and the hourly refresh reads nothing. Community: the plan gives it no (E) mark, and linking the ticket that tracks the work is basic record keeping. A ServiceNow or Halo ticket connection also needs preview (a preview feature), and a Halo one discovery.halo (enterprise), because it signs in through the Halo connection. Links to ServiceNow and Halo tickets are pasted, parsed and kept with a status set by hand either way
preview (the installation’s preview features are on; Phase 11 launch scope) yes yes yes Only while the installation sets SOFTWARDEN_PREVIEW_FEATURES (below): without the setting no organization has it, whatever an override says. ServiceNow and Halo ticket connections need it beside ticket_links (tickets/services.SYSTEM_CAPABILITY): without it GET /ticket-connections lists a kept ServiceNow or Halo connection with available: false and DELETE still removes it, while every other /ticket-connections... route on one, a create of one, a link refresh and create-ticket for those systems are 403 naming preview, GET /ticket-links/systems lists Jira only and the hourly refresh skips them. The SPA reads it to hide the preview features rather than call them another edition’s
billing yes not built (Phase 10; blocked until there is a Stripe account)
org.self_delete (an Admin deletes their own organization after an export and a grace period; Phase 10) yes POST /organization/deletion (in the service, after the role check; the capability test posts it). Reading the state and canceling aren’t gated, so a scheduled deletion can always be canceled. Self-hosted operators remove an organization themselves
org.tenant_backups (a nightly export of the organization kept for SOFTWARDEN_TENANT_BACKUP_RETENTION nights; Phase 10; CLOUD_AND_ENTERPRISE) yes yes the org.tenant_backups schedule queues backups only for organizations with it (the capability test uses the test-only route); backups are listed and downloaded with the exports, with this capability or org.export
sso.saml (SAML 2.0 single sign-on, ee/saml; Phase 8) yes the public routes auth/saml/<slug>/metadata, auth/saml/<slug>/acs and auth/saml/start (F-52: without it, or without SAML set up, start and the ACS redirect to /login?error=not_configured and the metadata answers 404, exactly as for an unknown organization; the capability test chooses the saml preset); on /sso/settings, choosing the saml preset or sending a SAML value (idp_*, label), testing and activating a SAML draft. Active SAML settings stop signing anyone in when the capability goes (core/sso.effective; the settings’ effective.source is capability) and saml (the SP addresses) is null without it
scim (SCIM 2.0 provisioning of users and groups, ee/scim; Phase 8) yes every /scim/v2/<slug>/... route (a SCIM 403 error with a valid token for an organization without it); the Admin routes /scim/tokens, /scim/users, /scim/groups (policy(capability=scim); the capability test reads the token list); PATCH /organization with scim_default_role or scim_role_groups. Existing provisioned users, groups and memberships stay when the capability goes; the identity provider just can’t call any more
assistant (Claude suggests link / create / dismiss for discovery candidates; Phase 6) yes every /assistant/... route (policy(capability=assistant)) and POST /discovery/observations/{id}/assistant/accept and /reject (in the service); without it candidates’ assistant_suggestion is null, no batch is queued after a run and a queued batch fails capability
discovery.azure yes POST /connectors with source=azure (source built in Phase 6)
discovery.powerplatform yes POST /connectors with source=powerplatform (Phase 6)
discovery.halo yes /connections/halo (Phase 6 Connections; the API test reads the list); POST /connectors with source=halo when the source lands
discovery.okta (Okta applications; Phase 11; a preview feature) yes every /connections/okta route (policy(capability=discovery.okta), Admin; the capability test reads the list), POST /connectors with source=okta, a run request, every run and every access check
discovery.google (third-party apps from the Google Workspace OAuth token audit log; Phase 11; a preview feature) yes every /connections/google_workspace route (policy(capability=discovery.google), Admin; the capability test reads the list), POST /connectors with source=google_workspace, a run request, every run and every access check. Not in the plan’s enterprise list, which names discovery.okta only: REVIEW.md §1 puts Google beside Okta in enterprise “advanced discovery”, and each discovery source is its own capability, so Google Workspace has its own name rather than riding on discovery.okta
audit.export (the audit log as JSON lines for a SIEM, with snapshots; Phase 3) yes GET /audit-events/export.jsonl (required_capability; Admin)
compliance_profile yes PATCH /organization with compliance_profile_enabled (Admin); while off, record responses omit the fields and writes or filters naming them get a field error

Community lists only features that exist; a phase that ships a community feature adds its name. A gated capability without a real endpoint is tested through a test-only route (tests/capability_urls.py, never in the shipped URLconf); when the feature ships, its entry in COVERAGE in tests/test_capabilities.py moves to the real endpoint. Adding a name to the enterprise or cloud set without a COVERAGE entry fails the suite.

Every route is held to its capability too (F-66): ROLE_LEDGER in tests/test_permissions.py gives every organization route, every method, its minimum role and its capability decision (- for none), so a new route fails until it has a line. tests/test_capabilities.py fails a route that declares a capability its line lacks, and calls every route whose line names one as the Admin of an organization without it (a community organization, or a platform revoke for a community capability): it must answer the capability problem naming it.

The plan’s enterprise list also names retention (configurable retention for audit events and notification logs). It isn’t built, so it isn’t in the enterprise edition until it is (release hardening C8): audit events are kept forever in every edition. AuditEventQuerySet.purge() stays as the deletion that feature will use (today only seed_scale calls it). A platform override naming retention stored before this is ignored.

In the API

  • GET /api/v1/auth/me returns edition and capabilities (sorted) for the current organization. The SPA gates on capabilities.

  • A gated request in an organization without the capability gets 403:

    {
      "type": "/problems/capability-required",
      "title": "This feature isn't enabled for this organization.",
      "status": 403,
      "detail": "This needs the discovery.azure capability, which this organization's edition doesn't include.",
      "capability": "discovery.azure"
    }

    The role is checked first, so a Viewer refused for role gets the role message.

  • In code: require_capability(org, name) after the role check, or has_capability(org, name) to branch. An unknown name raises ValueError, so a typo fails in tests rather than silently refusing.

Setting the edition

Only an operator sets it; no API endpoint writes it:

docker compose exec app python manage.py set_edition --list                 # slug, edition, name
docker compose exec app python manage.py set_edition <org-slug> enterprise   # audited as organization.edition_changed

On a development checkout run the same commands in apps/api as uv run python manage.py ..., with SOFTWARDEN_DEBUG=1 set (README, Run it locally).

Platform staff can do the same from the platform admin console (POST /api/v1/platform/organizations/{id}/edition, with a reason; audited in the organization and in the platform stream).

Per-organization overrides (feature flags)

Platform staff can grant or revoke one capability for one organization, with a reason and an optional expiry (POST /api/v1/platform/organizations/{id}/capability-overrides; Phase 10, runbook docs/04-development/platform-admin-runbook.md). capabilities(org) is then the edition’s set, plus live grants, minus live revokes, and every gate, auth/me included, follows it. The name must be one in the mapping above. The overrides are read once per request or job and a change applies from the next request, well within a minute. Overrides change names only; nothing compares editions.

Preview features

1.0.0 ships some features as previews, hidden unless the installation turns them on (owner decision 2026-11-10, launch scope): Okta and Google Workspace discovery, and ServiceNow and Halo ticket connections. One installation setting, SOFTWARDEN_PREVIEW_FEATURES (docs/ENV.md; off by default; Compose .env, Helm extraEnv), and one place that reads it, capabilities(org) in core/capabilities.py:

  • Every edition includes preview. While the setting is off, capabilities(org) drops preview and the capabilities that exist only as previews (PREVIEW_ONLY: discovery.okta, discovery.google) for every organization, after its edition and overrides, so a platform grant can’t bring them back. With it on, they follow the edition and overrides as usual (Okta and Google stay enterprise), and a platform revoke of preview hides them for one organization.
  • Every gate then follows: auth/me, the routes (403 /problems/capability-required, whose detail says it is a preview feature this installation hasn’t turned on), the services, the schedules and the SPA, which hides the Okta and Google sources, setup cards and connection tabs and the ServiceNow and Halo ticket connections when preview is missing, rather than calling them another edition’s.
  • Nothing is deleted. Okta and Google connectors stay in GET /connectors with available: false (as any connector whose source the organization lost): they can be turned off or deleted, never changed, run, checked or scheduled; their runs and observations stay; doctor reports them as “Unavailable (preview feature off)”, the Discovery summary marks them available: false, and GET /discovery/sources leaves the Okta and Google sources out. Okta and Google connections stay in the database, unreachable through the API. ServiceNow and Halo ticket connections are listed with available: false (the admin page: “Kept from a preview install”) and can only be deleted, which is audited with kept: true, so a kept Halo one never blocks deleting its Halo connection. ServiceNow and Halo links stay and new ones can be pasted, with a status set by hand. Turning the setting on gives the capabilities back and all of it works again, unchanged.
  • To release a preview feature, take its name out of PREVIEW_ONLY (and, for a ticket system, set its entry in tickets/services.SYSTEM_CAPABILITY to None).

Limits

Beside what an organization may use, core/limits.py caps how much it may create: records, other_records (people, vendors, contracts and the other secondary rows, together), connectors, members (active), attachment_bytes, and its API rates api_rate_read and api_rate_write (requests per minute for the organization as a whole). Defaults per edition (EDITION_LIMITS, the one place billing tiers will set them):

Limit Community Cloud Enterprise
records unlimited 5,000 unlimited
other_records unlimited 50,000 unlimited
connectors unlimited 10 unlimited
members unlimited 50 unlimited
attachment_bytes unlimited 5 GB unlimited
api_rate_read unlimited 1,200/min unlimited
api_rate_write unlimited 300/min unlimited

Platform staff override one organization’s value, with a reason and an optional expiry (POST /api/v1/platform/organizations/{id}/limits, value: null for unlimited; an API rate is at least 1). A create past a limit is 409 /problems/limit-reached naming the limit, its maximum and the usage (reactivating a deactivated member counts as adding one); other updates and deletes never are, and lowering a limit removes nothing. Members read their limits and usage at GET /api/v1/organization/limits. docs/API.md “Limits” lists every create that is checked. The operator’s break-glass recovery (manage.py reset_admin) can reactivate an account past the members limit; it removes nothing, and new member creates stay refused until usage is back under the limit.

There is no license verification yet. The organization’s license key slot (licence_key_ciphertext, key id, set at) exists for when it arrives and is never returned, audited or exported.

Configuring the assistant (enterprise)

The assistant suggests link, create or dismiss for undecided discovery candidates, with a confidence and a reason; Editors decide (Accept, Reject, or any decision as usual) and nothing is ever applied on its own.

  1. An Admin opens the assistant settings (GET/PATCH /api/v1/assistant/settings, If-Match supported), enters an Anthropic API key (api_key, write-only; or leaves it empty to use the installation’s key, when the installation offers it), sets the monthly cap (monthly_cap_usd, default 10.00 USD) and turns it on (enabled: true).
  2. Every successful discovery run that sees new or changed objects queues a batch for its connector; an Editor can ask for one with POST /api/v1/assistant/batches ({"connector": id}, or all). GET /api/v1/assistant/batches shows what each did and why one stopped (stop_reason: cap_reached is “cap reached”).
  3. GET /api/v1/assistant/usage?month=YYYY-MM (Admin) lists every API call with its four token counts and cost, and the month’s total against the cap.

Installation settings: SOFTWARDEN_ASSISTANT_MODEL (default claude-sonnet-5-5), SOFTWARDEN_ASSISTANT_PRICES (JSON, USD per million tokens per model: input, output, optional cache_read and cache_write; a model with no price is refused), ANTHROPIC_API_KEY (the installation’s key, for organizations without their own) and SOFTWARDEN_ASSISTANT_INSTALLATION_CAP_USD (the most each organization may spend on the installation’s key per month). The installation key is used only when the ceiling is set; an organization on it is held to the lower of its own cap and the ceiling (month.cap_usd; installation_key_available and installation_cap_usd in the settings). What is sent to Anthropic: docs/PERMISSIONS.md, “AI assistant: Anthropic”.