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 an S3 client plus object-handle helpers in every supported language.
Go
import (
"context"
"log"
gestalt "github.com/valon-technologies/gestalt/sdk/go"
)
ctx := context.Background()
s3, err := gestalt.S3()
if err != nil {
log.Fatal(err)
}
defer s3.Close()
avatar := s3.Object("uploads", "avatars/user-123.png")
if _, err := avatar.WriteBytes(ctx, pngBytes, &gestalt.WriteOptions{
ContentType: "image/png",
}); err != nil {
log.Fatal(err)
}
body, err := avatar.Bytes(ctx, nil)
if err != nil {
log.Fatal(err)
}
_ = bodyBuilding 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.jsonProvider interface
Implement the SDK’s authored S3Provider surface. The SDK adapts this typed
interface to the underlying gRPC service:
| Method | Purpose |
|---|---|
| Head object | Return metadata for one object reference. |
| Read object | Stream one object body with its metadata. |
| Write object | Accept a body stream and return committed metadata. |
| Delete object | Delete one object reference. |
| List objects | List objects by bucket, prefix, delimiter, and continuation token. |
| Copy object | Copy an object server-side between object references. |
| Presign object | Produce a presigned request URL plus required headers. |
Go
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, ref gestalt.ObjectRef, opts *gestalt.ReadOptions) (gestalt.ObjectMeta, io.ReadCloser, error) {
_ = opts
meta, err := p.HeadObject(ctx, ref)
if err != nil {
return gestalt.ObjectMeta{}, nil, err
}
return meta, io.NopCloser(bytes.NewReader(nil)), nil
}
func (p *Provider) WriteObject(ctx context.Context, ref gestalt.ObjectRef, body io.Reader, opts *gestalt.WriteOptions) (gestalt.ObjectMeta, error) {
_ = body
_ = opts
return p.HeadObject(ctx, ref)
}
func (p *Provider) DeleteObject(context.Context, gestalt.ObjectRef) error {
return nil
}
func (p *Provider) ListObjects(context.Context, gestalt.ListOptions) (gestalt.ListPage, error) {
return gestalt.ListPage{}, nil
}
func (p *Provider) CopyObject(ctx context.Context, source gestalt.ObjectRef, destination gestalt.ObjectRef, opts *gestalt.CopyOptions) (gestalt.ObjectMeta, error) {
_ = source
_ = opts
return p.HeadObject(ctx, destination)
}
func (p *Provider) PresignObject(context.Context, gestalt.ObjectRef, *gestalt.PresignOptions) (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:
- assetsFor packaging and published release URLs, see Releasing provider packages.
Implementation notes
- Treat object references as
{bucket, key, version_id}values, 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, andstorage_class. CopyObjectconditionals apply to the source object, not the destination object.- Map missing objects to
NotFound, conditional failures toFailedPrecondition, and invalid ranges toOutOfRangeso SDK error mapping stays portable. PresignObjectshould return only caller-required headers. Do not leak transport-generated headers such asHost.- 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/CopyObjectfailures.