File Repository Storage and Connections
File repositories are Hegemony's managed file store for firmware images, scripts, golden configurations, and other artifacts, backed by S3-compatible object storage. The database holds the catalog (repositories, logical folders, file metadata); the bytes live in object storage under content-addressed keys.
This document explains repository semantics — the managed internal repository, per-organization defaults, shared-org read-through, per-repository credentials, connection testing, and how repositories travel through Configuration Exchange.
For browsing files, uploading, and using repository files in flow steps, see the File Repositories guide.
A repository's name follows the platform's object name rules, and is unique per organization.
Key Principles
One catalog, org-scoped rows — every repository belongs to exactly one organization. There are no platform-global repositories; platform-wide artifacts live in the designated shared organization instead.
Credentials are never stored in the database — a repository either uses the platform's deployment credentials (
HEGEMONY_S3_*) or carries secret refs ({{ secret('…') }}) that are resolved just-in-time per operation.env()andfile()are refused: they would read the API's own credentials.Files are UUID-addressed — flows and steps reference stored files by id (
file_idsin step configs), not by path or URL. Deleting a file that a flow still references is refused.
The managed internal repository
Every organization is auto-provisioned an Internal object storage repository pointing at the deployment's object store (the bundled Versity S3 Gateway by default), isolated by an org-scoped key prefix (files/orgs/<slug> for non-default orgs). These rows carry managed: true:
- Their connection settings (kind, endpoint, bucket, prefix, config) are owned by the environment — re-applied from
HEGEMONY_S3_*on every boot, locked against API edits (409) and Configuration Exchange overrides (ignored with a log). - They stay in platform-credential mode forever; the boot reconcile wipes any injected per-repo config.
- They cannot be deleted (409 via API,
OPERATOR_ACTION_REQUIREDvia CX); org deletion removes an empty internal repository automatically. - A user-created repository that merely reuses the name is never adopted or modified, even if it points at the same endpoint and bucket. If one already holds Internal object storage when the managed row is provisioned, the managed row is created as Internal object storage (platform) (a numbered variant if that is taken too) and takes the default name back once it is free.
The managed repository was called Internal MinIO before the bundled store changed. The rename is applied to every organization's managed row on the first boot after the upgrade (the default organization's at startup, the others by the file_repository_backfill maintenance job), together with the connection settings from HEGEMONY_S3_*. Configuration Exchange bundles and sync profiles address file repositories by name, so any that still say Internal MinIO must be re-exported or edited before they are applied again; the API does not alias the old name.
Deployment settings:
| Variable | Meaning |
|---|---|
HEGEMONY_S3_INTERNAL_ENABLED | false turns the whole mechanism off: no bucket creation or seeding at startup, and no managed repository provisioned at boot, on org creation, or by the backfill job |
HEGEMONY_S3_ENDPOINT_URL | Platform S3 endpoint (in-cluster) |
HEGEMONY_S3_EXTERNAL_URL | Client-reachable base substituted into presigned URLs |
HEGEMONY_S3_REGION | Region for the platform connection (default us-east-1) |
HEGEMONY_S3_ACCESS_KEY_ID / HEGEMONY_S3_SECRET_ACCESS_KEY | Platform credential set |
HEGEMONY_S3_BUCKET | Platform bucket (default hegemony) |
HEGEMONY_S3_PREFIX | Base object-key prefix (default files) |
HEGEMONY_S3_PRESIGN_EXPIRY_SECONDS | Presigned URL lifetime |
HEGEMONY_S3_EXTRA_BUCKETS | Extra buckets the API creates in the configured platform object store at startup (the bundled gateway or an external S3 endpoint) |
HEGEMONY_S3_SEED_DIR | Directory whose <bucket>/<key…> tree the API uploads at startup (demo seeding) |
One default per organization
Each org has at most one default repository, enforced by a partial unique index (migration 037). The default is switched only via the set-default endpoint (or provisioning); creating an org's first repository makes it the default automatically. Concurrent default changes surface as retryable 409s.
Shared-organization read-through
When a shared organization is designated, its repositories are visible to every org — strictly read-only:
- Readable for tenants: listing, browsing folders, file metadata, downloads, presigned URLs.
- Strict-own-org (404 on shared rows): uploads, imports, folder create/rename/delete, file move/delete, repository edit/delete/set-default, and test-connection.
A shared repository may carry per-repo credential refs. Those refs are resolved in the owning (shared) org's secret namespace — a tenant triggering a download through the read-through uses the shared org's credentials without ever being able to read them:
- Responses for shared rows redact the sensitive config keys (the credential refs); only
access_key_id_ref_set/secret_access_key_ref_setremain, so UIs can still say "credentials configured". - Resolution errors surfaced to tenants carry the repository name and an exception class name only — never refs, paths, or values.
- Test-connection 404s on shared rows so tenants cannot probe the owning org's credentials.
Storage kinds and per-repository connections
Repositories have a kind (today only s3) registered in the storage-kind registry (apps/api/services/storage). Each kind contributes its config model (JSON schema served by GET /file-repositories/kinds), a connection resolver, and a connection test. Additional kinds can register through the hegemony.storage_kinds entry-point group.
The s3 kind's config:
| Key | Meaning |
|---|---|
access_key_id_ref / secret_access_key_ref | Secret template refs (both or neither; plaintext is rejected) |
region | Region for per-repo connections (defaults to the platform region) |
external_url | Client-reachable base substituted into this repository's presigned URLs |
allow_insecure_url | Per-repository opt-out of the SSRF guard (http:// / private endpoints) |
Credential modes:
- Platform mode (empty config / no refs): operations use the
HEGEMONY_S3_*connection, and the repository's endpoint URL must equal the platform endpoint (422 otherwise). - Per-repo mode (both refs set): any endpoint is accepted after the SSRF check (DNS-resolved private/loopback targets rejected unless
allow_insecure_url). Credentials are resolved per operation in the owning org's namespace and never logged, persisted, or returned.
Presigned download URLs (/files/{id}/download-url, /files/{id}/device-download-url) rewrite the signing endpoint to the connection's external URL. Only the platform connection depends on HEGEMONY_S3_EXTERNAL_URL (503 when unset); a per-repo connection without an external_url returns the URL signed against its endpoint as-is.
Connection testing
POST /file-repositories/{id}/test-connection probes the bucket (head_bucket + a one-key list under the repository prefix) with the repository's effective connection. It always returns HTTP 200; failures are encoded as ok: false with an error_kind of auth, network, bucket_not_found, endpoint_invalid, or unknown. Successful tests stamp last_connected_at, as does the startup reachability probe for the platform endpoint.
File usage guard
Flow steps hold stored-file ids in their configs (file_ids). DELETE /files/{id} scans live flow drafts plus each flow's committed version (historical versions never block) and refuses with 409 while references exist, naming the caller org's flows and counting foreign-org references without disclosing their names. GET /files/{id}/usage returns the same information read-only.
Configuration Exchange
Repository bundles (file_repositories/*.yaml) carry name, kind, endpoint, bucket, prefix, config, and the default flag. Imports enforce the same rules as the API: kind-config validation (including plaintext-credential rejection) and the endpoint-mode policy, so a bundle cannot smuggle in what the API would refuse. Credential refs are template expressions and travel verbatim; managed rows ignore bundle connection settings entirely, and deleting a managed or non-empty repository via CX is refused.
History card
The detail page carries a History card listing the most recent audit log entries about this object: how long ago, what the action was, which fields changed, who did it, and a non-success outcome where there was one. A row opens the same entry dialog the audit log uses, and the card links into the log filtered to this object. It appears only for callers who may read the audit log, so an operator or org admin sees the page without it. The page itself is open to every signed-in user, like its tile on the Settings hub; Edit, Test Connection, Set Default and Delete are enabled only for callers the API allows and shown locked otherwise.
Related docs
- Secrets Management — reference formats and backends used by credential refs
- Internal OpenBao — where org-scoped secret namespaces live
- Glossary — File Repository