Summary
Add object storage (S3-compatible) as a Tier-1 managed data service, alongside Postgres / MySQL / MariaDB / Valkey — a substrate apps declare a dependency on, provisioned by the brain and injected as MALMO_SERVICE_S3_*, rather than each app bundling its own engine.
Motivation
Object/blob storage is the next substrate modern self-hosted apps assume. First concrete driver: Langfuse (store#45), whose v3 ingestion pipeline requires S3 — every event is written to blob storage before the worker processes it into ClickHouse (LANGFUSE_S3_EVENT_UPLOAD_*), and media/multimodal payloads use it too. It is mandatory infra, not optional. More will follow (media servers, backup targets, anything storing user files).
Same rationale as the existing managed types: host the substrate apps already use rather than have every app ship + operate its own copy. An S3 endpoint behind MALMO_SERVICE_S3_* keeps apps portable (standard S3 API + env-var surface) and lets us change the engine later without touching a single app.
Proposed shape (mirror Postgres/Valkey)
- Manifest declares e.g.
services.storage: { type: s3 }.
- Brain provisions/reuses a shared instance on a dedicated internal network (
malmo-svc-s3-*, s3-*.malmo.internal), lazy spin-up, drop-on-uninstall grace period — identical lifecycle to the SQL/Valkey types.
- Injects the standard contract:
MALMO_SERVICE_STORAGE_ENDPOINT, _ACCESS_KEY_ID, _SECRET_ACCESS_KEY, _BUCKET, _REGION — the app's compose maps them to whatever env vars it expects (Langfuse: LANGFUSE_S3_*).
- Provisioning via a throwaway one-shot client container joined to the service network (per DECISIONS 2026-06-15), not a brain-side S3 client — same pattern as
psql/valkey-cli.
Open question — per-app isolation model
The SQL types isolate via role+database; Valkey via ACL-user on a shared keyspace. Object storage needs a decision (blocks the # Per-app isolation contract):
- Bucket-per-app + a scoped access key limited to that bucket (IAM-style policy). Cleanest boundary; depends on engine policy support.
- Prefix-per-app in a shared bucket + a key scoped to the prefix. Weaker; some engines can't scope keys to a prefix.
- Confidentiality boundary must hold the same standard as the other managed types (an app can't read another app's objects).
Engine — decision needed (license-verified 2026-07-15)
| Engine |
License |
Notes |
MinIO (cgr.dev/chainguard/minio) |
AGPLv3 |
De-facto self-hosted S3; API devs already know. Boots clean under cap_drop: ALL as non-root 65532 (probed). AGPL permits managed/hosted use when shipped unmodified (§13 satisfied by upstream source). Caveats: community edition is console-stripped since the 2025 AIStor rebrand (headless S3 API only), and copyleft binds us to shipping it unmodified. |
| SeaweedFS |
Apache-2.0 |
S3-compatible; no copyleft friction. |
| Scality CloudServer (Zenko) |
Apache-2.0 |
S3-compatible; no copyleft friction. |
Prior art cuts both ways: the Valkey-not-Redis and declined-Mongo decisions established a preference for permissive engines when malmo serves the substrate as a service (DECISIONS 2026-06-13, 2026-06-25). AGPL is not SSPL and is technically compliant here, but a permissive engine (SeaweedFS / CloudServer) gets the same S3 API without the copyleft/roadmap exposure. Because everything rides the standard S3 interface behind MALMO_SERVICE_S3_*, the engine choice is deferrable and reversible — apps don't change if we swap it. Recommend picking the engine at build time; leaning permissive unless MinIO-specific familiarity is judged worth the AGPL/console tradeoffs.
Boot evidence (already probed)
cgr.dev/chainguard/minio (RELEASE.2026-06-04) boots and serves under --cap-drop ALL --security-opt no-new-privileges as --user 65532:65532 over a pre-chowned bind; ports 9000/9001 both >1024 → no setcap/low-port exec trap. No sandbox blockers for the MinIO path. (SeaweedFS/CloudServer not yet probed.)
Threshold note
Tier-1 policy adds a type when 3+ store apps justify it (SERVICE_PROVISIONING.md # Additional managed data services). Today the count is 1 (Langfuse). This issue tracks the design; suggest not building until the 2nd/3rd S3-needing app lands, so the per-app isolation + engine decisions are informed by real app requirements. Until then, an S3-needing app bundles a permissive engine as its own sidecar (the Mongo/bundled-engine pattern).
References
Summary
Add object storage (S3-compatible) as a Tier-1 managed data service, alongside Postgres / MySQL / MariaDB / Valkey — a substrate apps declare a dependency on, provisioned by the brain and injected as
MALMO_SERVICE_S3_*, rather than each app bundling its own engine.Motivation
Object/blob storage is the next substrate modern self-hosted apps assume. First concrete driver: Langfuse (store#45), whose v3 ingestion pipeline requires S3 — every event is written to blob storage before the worker processes it into ClickHouse (
LANGFUSE_S3_EVENT_UPLOAD_*), and media/multimodal payloads use it too. It is mandatory infra, not optional. More will follow (media servers, backup targets, anything storing user files).Same rationale as the existing managed types: host the substrate apps already use rather than have every app ship + operate its own copy. An S3 endpoint behind
MALMO_SERVICE_S3_*keeps apps portable (standard S3 API + env-var surface) and lets us change the engine later without touching a single app.Proposed shape (mirror Postgres/Valkey)
services.storage: { type: s3 }.malmo-svc-s3-*,s3-*.malmo.internal), lazy spin-up, drop-on-uninstall grace period — identical lifecycle to the SQL/Valkey types.MALMO_SERVICE_STORAGE_ENDPOINT,_ACCESS_KEY_ID,_SECRET_ACCESS_KEY,_BUCKET,_REGION— the app's compose maps them to whatever env vars it expects (Langfuse:LANGFUSE_S3_*).psql/valkey-cli.Open question — per-app isolation model
The SQL types isolate via role+database; Valkey via ACL-user on a shared keyspace. Object storage needs a decision (blocks the # Per-app isolation contract):
Engine — decision needed (license-verified 2026-07-15)
cgr.dev/chainguard/minio)cap_drop: ALLas non-root 65532 (probed). AGPL permits managed/hosted use when shipped unmodified (§13 satisfied by upstream source). Caveats: community edition is console-stripped since the 2025 AIStor rebrand (headless S3 API only), and copyleft binds us to shipping it unmodified.Prior art cuts both ways: the Valkey-not-Redis and declined-Mongo decisions established a preference for permissive engines when malmo serves the substrate as a service (DECISIONS 2026-06-13, 2026-06-25). AGPL is not SSPL and is technically compliant here, but a permissive engine (SeaweedFS / CloudServer) gets the same S3 API without the copyleft/roadmap exposure. Because everything rides the standard S3 interface behind
MALMO_SERVICE_S3_*, the engine choice is deferrable and reversible — apps don't change if we swap it. Recommend picking the engine at build time; leaning permissive unless MinIO-specific familiarity is judged worth the AGPL/console tradeoffs.Boot evidence (already probed)
cgr.dev/chainguard/minio(RELEASE.2026-06-04) boots and serves under--cap-drop ALL --security-opt no-new-privilegesas--user 65532:65532over a pre-chowned bind; ports 9000/9001 both >1024 → nosetcap/low-port exec trap. No sandbox blockers for the MinIO path. (SeaweedFS/CloudServer not yet probed.)Threshold note
Tier-1 policy adds a type when 3+ store apps justify it (
SERVICE_PROVISIONING.md# Additional managed data services). Today the count is 1 (Langfuse). This issue tracks the design; suggest not building until the 2nd/3rd S3-needing app lands, so the per-app isolation + engine decisions are informed by real app requirements. Until then, an S3-needing app bundles a permissive engine as its own sidecar (the Mongo/bundled-engine pattern).References
malmoos/storeHealth detector: brain-db-corrupt (locus C, PRAGMA integrity_check) #45 ([Catalog] Add Langfuse)docs/specs/SERVICE_PROVISIONING.md— # Tier 1, # Additional managed data services, # Per-app isolationDECISIONS.md2026-06-13 (Valkey/Redis), 2026-06-25 (Mongo declined)