Skip to content

Storage Layer

The ABC is split by domain: PlatformStorageBackend (identity, orgs, admin, observability) and ProductStorageBackend (resources, findings, jobs, analysis configuration). StorageBackend is simply both, and one MongoBackend / one InMemoryStorage implement it — one connection and one database still serve both halves. The split exists so the multi-tenant half can be depended on without the analysis half; to back only that, subclass PlatformStorageBackend alone.

classDiagram
    class StorageLifecycle {
        <>
        +setup()
        +close()
    }
    class PlatformStorageBackend {
        <>
        +create_organization() / get_organization() / update_organization()
        +create_user() / get_user() / get_user_by_email()
        +create_membership() / get_membership() / list_members()
        +create_api_key() / get_api_key_by_hash()
        +list_api_keys(org_id) / delete_api_key(org_id, key_id)
        +create_team() / list_teams() / add_team_member()
        +create_invite() / get_invite_by_token_hash() / revoke_invite()
        +create_sso_connection() / create_scim_config()
        +get_platform_settings() / set_platform_settings()
        +record_usage_event() / list_usage_events() / get_monthly_llm_cost(org_id)
        +record_span() / query_spans() / get_metrics_summary() / get_key_usage()
        +record_audit_event() / query_audit_events() "org_id=None means all orgs"
        +apply_retention_ttls(retention) "collMod on metrics/usage_events/audit_events ttl_idx -- no-op on InMemoryStorage"
        +get_retention_sweep_state() / set_retention_sweep_state() "the sweep primitive the product registers steps into"
        +create_job() / get_job() / update_job() / list_jobs() / count_jobs() "update_job always stamps updated_at = now, in addition to whatever fields it's given"
        +sweep_stuck_jobs(older_than) "marks jobs with no progress update since older_than as failed, across every org"
        +get_job_sweep_state() / set_job_sweep_state()
        +delete_organization(org_id) "call last, after every bulk delete"
        +delete_X_for_org(org_id) "platform half: memberships/api_keys/team_members/teams/org_invites/jobs"
    }
    class ProductStorageBackend {
        <>
        +create_resource(org_id, hash, value)
        +read_resource(org_id, hash)
        +set_analysis(org_id, hash, analysis)
        +list_installed_analyzers(org_id) / set_analyzer_enabled()
        +create_custom_rule() / get_custom_rule() / list_custom_rules() / update_custom_rule() / delete_custom_rule()
        +create_report_template() / get_report_template() / list_report_templates() / update_report_template() / delete_report_template()
        +create_llm_credential() / get_llm_credential() / list_llm_credentials() / delete_llm_credential()
        +upsert_finding(initial_status) returns created:bool / get_finding() / list_findings() / list_findings_by_hashes() / update_finding_status() / get_findings_summary()
        +create_integration() / get_integration() / get_integration_by_installation_id() / delete_integration()
        +purge_findings(statuses, severities, older_than) "callers only ever pass terminal statuses"
        +redact_stale_resources(older_than) "overwrites value, leaves analysis/hash intact"
        +delete_X_for_org(org_id) "product half: resources/namespace_entries/installed_analyzers/custom_rules/report_templates/rule_categories/llm_credentials/findings"
    }
    class StorageBackend {
        <>
        +inherits both halves "what a single-process OpenTremor deployment needs"
    }
    class InMemoryStorage {
        -dict _orgs
        -dict _users
        -dict _memberships
        -dict _resources "keyed by org_id:hash, incl. last_ingested_at"
        -dict _entries "keyed by (org_id, namespace)"
        -dict _api_keys
        -dict _llm_credentials
        -dict _jobs
        -list _usage_events
        -dict _findings "keyed by (org_id, hash, rule_id)"
        -dict _integrations "keyed by (org_id, provider)"
        -dict _manifest_states "keyed by state_id, looked up by state_hash"
        -deque _spans
        -deque _audit_events "maxlen 50k, bigger than _spans' 10k -- lower volume per request"
        -dict _retention_sweep_state "None until a sweep has ever run"
        -dict _jobs "job_id -> doc (platform half)"
        -dict _job_sweep_state "None until a stuck-job sweep has ever run (platform half)"
    }
    class MongoBackend {
        -Collection orgs_col
        -Collection users_col
        -Collection memberships_col
        -Collection res_col "_id = org_id:hash"
        -Collection ent_col "_id = org_id:namespace:hash"
        -Collection keys_col
        -Collection llm_credentials_col "_id = org_id:provider"
        -Collection usage_events_col "TTL-indexed"
        -Collection findings_col "_id = org_id:hash:rule_id"
        -Collection integrations_col "_id = org_id:provider"
        -Collection github_manifest_states_col "unique idx on state_hash, TTL on expires_at"
        -Collection metrics_col
        -Collection audit_events_col "TTL-indexed on its own audit_retention_days -- no longer shares retention_days with metrics_col"
        -Collection retention_sweep_state_col "singleton, fixed _id"
        -Collection jobs_col "platform half"
        -Collection job_sweep_state_col "singleton, fixed _id (platform half)"
        -int retention_days "telemetry/usage TTL, seconds"
        -int audit_retention_days
    }
    StorageLifecycle <|-- PlatformStorageBackend
    StorageLifecycle <|-- ProductStorageBackend
    PlatformStorageBackend <|-- StorageBackend
    ProductStorageBackend <|-- StorageBackend
    StorageBackend <|-- InMemoryStorage : implements
    StorageBackend <|-- MongoBackend : implements

usage_events and audit_events are deliberately excluded from the org-offboarding cascade — orphaned by org_id rather than deleted, so the audit trail survives the thing it describes.

Route handlers only ever see StorageBackend through FastAPI’s Depends — nothing in a router imports InMemoryStorage or MongoBackend directly, which is what makes swapping the live backend (see POST /admin/storage/switch above) possible without touching route code.

create_integration/get_integration/delete_integration are reused verbatim for the needs-review webhook (provider="review_webhook") — no new collection or storage method was needed, since it’s the same (org_id, provider) shape GitHub already uses. That reuse is what let outbound webhooks become a platform primitive (send_org_webhook) without a schema of their own: provider doubles as the webhook’s name, so one org can hold several independent endpoints.