Skip to Content

S3

S3 providers expose portable object storage to executable apps. Gestalt models the de facto S3-compatible object API rather than a filesystem API, so the same app code can target AWS S3, MinIO, GCS XML interoperability mode, Cloudflare R2, and other compatible backends.

Using S3 from a plugin

The SDK exposes a generated S3 client in every supported language. The client connects to one configured S3 binding by name; the backing bucket comes from provider configuration, so object references carry only key and an optional version_id. Object reads and writes are streamed: writes open with object metadata and then send byte chunks, and reads return object metadata plus a byte-chunk stream.

import ( "context" "io" "log" "github.com/valon-technologies/gestalt/sdk/go/client" ) ctx := context.Background() s3, err := client.ConnectS3(ctx, "uploads") if err != nil { log.Fatal(err) } avatar := &client.S3ObjectRef{Key: "avatars/user-123.png"} stream, err := s3.WriteObject(ctx, &client.WriteObjectOpen{ Ref: avatar, ContentType: "image/png", }) if err != nil { log.Fatal(err) } if err := stream.Send(pngBytes); err != nil { log.Fatal(err) } if _, err := stream.CloseAndRecv(); err != nil { log.Fatal(err) } meta, data, err := s3.ReadObject(ctx, &client.ReadObjectRequest{Ref: avatar}) if err != nil { log.Fatal(err) } var body []byte for { chunk, err := data.Recv() if err == io.EOF { break } if err != nil { log.Fatal(err) } body = append(body, chunk...) } _ = meta _ = body

Building your own S3 provider

Manifest

An S3 provider manifest declares kind: s3:

kind: s3 source: github.com/acme/gestalt-providers/s3/minio version: 0.0.1-alpha.1 displayName: MinIO S3 description: S3-compatible object storage provider backed by MinIO. spec: configSchemaPath: ./config.schema.json

Provider interface

Implement the SDK’s authored S3Provider surface. The SDK adapts this typed interface to the underlying gRPC service:

MethodPurpose
Head objectReturn metadata for one object reference.
Read objectStream one object body with its metadata.
Write objectAccept a body stream and return committed metadata.
Delete objectDelete one object reference.
List objectsList objects in the configured bucket by prefix, delimiter, and continuation token.
Copy objectCopy an object server-side between object references.
Presign objectProduce a presigned request URL plus required headers.
package s3provider import ( "bytes" "context" "io" "time" gestalt "github.com/valon-technologies/gestalt/sdk/go" ) type Provider struct{} func New() *Provider { return &Provider{} } func (p *Provider) Configure(_ context.Context, _ string, config map[string]any) error { _ = config return nil } func (p *Provider) HeadObject(ctx context.Context, ref gestalt.ObjectRef) (gestalt.ObjectMeta, error) { _ = ctx return gestalt.ObjectMeta{ Ref: ref, ETag: "", Size: 0, ContentType: "application/octet-stream", LastModified: time.Now(), Metadata: map[string]string{}, }, nil } func (p *Provider) ReadObject(ctx context.Context, req gestalt.ReadRequest) (gestalt.ReadResult, error) { meta, err := p.HeadObject(ctx, req.Ref) if err != nil { return gestalt.ReadResult{}, err } return gestalt.ReadResult{ Meta: meta, Body: io.NopCloser(bytes.NewReader(nil)), }, nil } func (p *Provider) WriteObject(ctx context.Context, req gestalt.WriteRequest) (gestalt.ObjectMeta, error) { _ = req.Body return p.HeadObject(ctx, req.Ref) } func (p *Provider) DeleteObject(context.Context, gestalt.ObjectRef) error { return nil } func (p *Provider) ListObjects(context.Context, gestalt.ListRequest) (gestalt.ListPage, error) { return gestalt.ListPage{}, nil } func (p *Provider) CopyObject(ctx context.Context, req gestalt.CopyRequest) (gestalt.ObjectMeta, error) { _ = req.Source return p.HeadObject(ctx, req.Destination) } func (p *Provider) PresignObject(context.Context, gestalt.PresignRequest) (gestalt.PresignResult, error) { return gestalt.PresignResult{ URL: "https://example.invalid/object", Method: gestalt.PresignMethodGet, ExpiresAt: time.Now().Add(time.Minute), Headers: map[string]string{}, }, nil }

gestalt.S3Provider adds runtime lifecycle wiring and a serve() helper for languages that expose the server-side S3 provider surface.

App access

Plugin-side S3 usage is documented in Using S3 from a plugin. Provider authors implement the S3 service surface; Gestalt handles configured plugin bindings, SDK sockets, and host-mediated object access URLs.

Deploying the provider

Custom S3 providers are wired into Gestalt the same way as first-party ones:

providers: s3: assets: source: ./providers/s3/custom/manifest.yaml config: endpoint: http://127.0.0.1:9000 region: us-east-1 apps: media: source: ./apps/media/manifest.yaml s3: - assets

For packaging and published release URLs, see Releasing provider packages.

Implementation notes

  • Treat object references as {key, version_id} values scoped to the provider’s configured bucket, not filesystem paths.
  • Keep reads and writes streaming. Do not require the caller to buffer the full object in memory.
  • Preserve backend metadata that fits the portable model: etag, size, content_type, last_modified, metadata, and storage_class.
  • CopyObject conditionals apply to the source object, not the destination object.
  • Map missing objects to NotFound, conditional failures to FailedPrecondition, and invalid ranges to OutOfRange so SDK error mapping stays portable.
  • PresignObject should return only caller-required headers. Do not leak transport-generated headers such as Host.
  • Implement multipart upload and multipart copy internally when the backend needs them, or reject above the single-request S3 limit explicitly. Do not silently rely on backend-specific PutObject / CopyObject failures.