The Platform Base
opentremor-platform is the multi-tenant half of OpenTremor, split out as its own package and
repository. It provides users, organizations, memberships, local/SSO/SCIM authentication,
teams, invitations, service-account API keys, platform admin (organizations, users, the audit
log, storage management, live-editable settings), pricing/billing/metering, telemetry and the
audit trail — and no opinion about what your product does.
opentremor-core is the reference consumer: the security-analysis engine, mounted on top.
Why it is separate
Section titled “Why it is separate”Core used to bundle two unrelated things: multi-tenant SaaS plumbing, and infrastructure-as-code security analysis. Only the second is specific to OpenTremor. Splitting them means a future project can depend on the first and get “multi-tenant SaaS with users, orgs, SSO, billing and an admin panel” without inheriting an analysis engine it has no use for.
The split is real, not notional: the platform’s test suite runs in an environment where
opentremor-core is not installed at all.
Using it
Section titled “Using it”from opentremor_platform.controllers.app import create_platform_app
app = create_platform_app() # a complete, runnable multi-tenant APIRun it directly:
CONFIG_FILE=configs/opentremor-platform-local.yaml \ uvicorn opentremor_platform.controllers.app:app --reloadConfiguration
Section titled “Configuration”Two example configs ship with the repo: configs/opentremor-platform-local.yaml (in-memory,
for development) and configs/opentremor-platform.yaml (MongoDB, with every platform
collection named).
Only storage, cors, auth, jwt, two_factor, llm.credential_encryption_key and
deployment are read from the file —
everything else is managed live through GET/PATCH /admin/settings and reset to its
hardcoded default at boot, so a value for those sections in the file is silently discarded.
That is deliberate: it means two identically-unconfigured instances can never disagree about
what a setting is. See Configuration reference
for the full rationale — it applies identically to a platform-only deployment.
The platform’s own config model is PlatformConfig; a host that mounts a product widens it
(see Sections that span both halves) and passes its own
model to create_platform_app(config=...).
Mounting a product on top
Section titled “Mounting a product on top”create_platform_app() returns a FastAPI application you add your own routers to. Everything
your domain contributes goes through a registration point, so you never edit the platform:
| What your product contributes | Where it registers |
|---|---|
| Admin-editable config sections | controllers/services/settings_registry.py |
| Retention/cleanup sweep steps | controllers/services/sweep_registry.py |
| Rows to delete when an organization is purged | controllers/services/org_purge_registry.py |
| MongoDB index declarations | controllers/storage/index_registry.py |
| “a new organization was created” seeding | controllers/shared/org_bootstrap.py |
| One-time startup seeding | controllers/shared/startup.py |
| A storage backend spanning both halves | controllers/storage/mongo_admin.configure_storage_classes() |
This mirrors the analyzer plugin system, which has always worked this way — the base defines an interface, installed packages register against it.
Sections that span both halves
Section titled “Sections that span both halves”Three config sections legitimately belong to both: storage.mongodb (collection names),
server (public URLs vs. ingest limits) and retention (telemetry/audit TTLs vs. the product’s
own purge windows). Each is declared by the platform at its own narrower type and widened by the
host, so a combined deployment still sees one flat config at the same attribute paths.
Keeping retention whole is deliberate: it means one sweep, with one state document, one
endpoint and one task-scheduler entry, rather than two of each.
llm is a platform section despite the name, and it is the base only: llm.enabled, and
llm.credential_encryption_key — the Fernet key this package encrypts SSO connection client
secrets under, and that a mounted product typically reuses for its own per-org provider
credentials.
What stays with the product is everything that isn’t generic: the provider clients, which force
a response schema the product defines. This split is why a platform-only deployment can use SSO
at all — before it, routers/sso.py reached for a config section only opentremor-core
declared.
llm.enabled says whether this deployment offers LLM-backed features. Enforce it with the
require_llm_enabled dependency from controllers/dependencies.py on any route that cannot work
without a provider call; it answers 501 Not Implemented, since no credential or permission
would make the capability appear.
It is admin-editable (PATCH /admin/settings {"llm_enabled": false}), taking effect on the
next request with no restart — so, like every admin-editable field, a value for it in the config
file is discarded at boot. Its sibling credential_encryption_key is not editable and is never
returned by the API.
That is the section’s one unusual property, and the registry supports it directly: register()
takes the model declaring a domain’s editable fields, so registering LLMToggleConfig rather
than LLMConfig leaves the key unregistered. An unregistered field is absent from the PATCH
body model, absent from effective, and left at its file/env value by
with_hardcoded_platform_defaults() — which is what stops a boot from replacing an operator’s
key with a fresh random one. See SettingsRegistry.editable_fields().
Storage
Section titled “Storage”PlatformInMemoryStorage (dev/tests) and PlatformMongoBackend (Motor) both implement
PlatformStorageBackend in full, so a platform-only deployment gets working persistence rather
than an interface it has to implement itself. PlatformMongoBackend.setup() creates exactly the
platform’s own indexes.
A host that composes both halves builds its backend from PlatformMongoBackend plus its own
mixin, and registers it so live storage switching hands the running app a backend that still has
every method the product calls.
Licence
Section titled “Licence”GNU AGPL v3 or later, with the OpenTremor Analyzer Plugin Exception — the same as the rest of the
OpenTremor server. See LICENSE and LICENSE-EXCEPTION in the repo root.