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/mereturnseditionandcapabilities(sorted) for the current organization. The SPA gates oncapabilities. -
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, orhas_capability(org, name)to branch. An unknown name raisesValueError, 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)dropspreviewand 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 ofpreviewhides 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 whenpreviewis missing, rather than calling them another edition’s. - Nothing is deleted. Okta and Google connectors stay in
GET /connectorswithavailable: 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 themavailable: false, andGET /discovery/sourcesleaves 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 withavailable: false(the admin page: “Kept from a preview install”) and can only be deleted, which is audited withkept: 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 intickets/services.SYSTEM_CAPABILITYto 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.
- 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). - 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/batchesshows what each did and why one stopped (stop_reason: cap_reachedis “cap reached”). 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”.