Skip to content

Managed service: object storage (S3-compatible) — Tier-1 candidate #330

Description

@onel

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions