S3
This page covers building custom S3 providers. If you only need to configure an object store for a deployment, use Providers > S3.
Gestalt’s S3 provider contract models the portable S3-compatible object API: object metadata, streaming reads and writes, prefix listing, server-side copy, and presigning. It intentionally excludes bucket administration, IAM policy management, and other control-plane features that do not transport cleanly across S3-compatible backends.
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 S3 gRPC service:
| Method | Purpose |
|---|---|
HeadObject | Return metadata for one object reference. |
ReadObject | Stream one object: metadata frame first, then byte frames. |
WriteObject | Accept one metadata frame followed by byte frames, then return committed metadata. |
DeleteObject | Delete one object reference. |
ListObjects | List objects by bucket, prefix, delimiter, and continuation token. |
CopyObject | Copy an object server-side between object references. |
PresignObject | Produce a presigned request URL plus required headers. |
Go
package s3provider
import (
"context"
gestalt "github.com/valon-technologies/gestalt/sdk/go"
proto "github.com/valon-technologies/gestalt/sdk/go/gen/v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
type Provider struct{}
func (p *Provider) Configure(_ context.Context, _ string, config map[string]any) error {
_ = config
return nil
}
func (p *Provider) HeadObject(ctx context.Context, req *proto.HeadObjectRequest) (*proto.HeadObjectResponse, error) {
_ = ctx
_ = req
return nil, status.Error(codes.Unimplemented, "todo")
}
func (p *Provider) ReadObject(req *proto.ReadObjectRequest, stream proto.S3_ReadObjectServer) error {
_ = req
_ = stream
return status.Error(codes.Unimplemented, "todo")
}
func (p *Provider) WriteObject(stream proto.S3_WriteObjectServer) error {
_ = stream
return status.Error(codes.Unimplemented, "todo")
}
func (p *Provider) DeleteObject(context.Context, *proto.DeleteObjectRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "todo")
}
func (p *Provider) ListObjects(context.Context, *proto.ListObjectsRequest) (*proto.ListObjectsResponse, error) {
return nil, status.Error(codes.Unimplemented, "todo")
}
func (p *Provider) CopyObject(context.Context, *proto.CopyObjectRequest) (*proto.CopyObjectResponse, error) {
return nil, status.Error(codes.Unimplemented, "todo")
}
func (p *Provider) PresignObject(context.Context, *proto.PresignObjectRequest) (*proto.PresignObjectResponse, error) {
return nil, status.Error(codes.Unimplemented, "todo")
}
func main() {
if err := gestalt.ServeS3Provider(context.Background(), &Provider{}); err != nil {
panic(err)
}
}Python S3 providers implement the generated HeadObject, ReadObject,
WriteObject, DeleteObject, ListObjects, CopyObject, and
PresignObject methods from gestalt.gen.v1.s3_pb2_grpc.S3Servicer.
gestalt.S3Provider adds the runtime lifecycle wiring and serve() helper.
Plugin SDK
Plugins consume S3 providers through the SDK, not by speaking gRPC directly. Each SDK exposes a client plus object-handle helpers:
s3, err := gestalt.S3()
if err != nil {
return err
}
defer s3.Close()
obj := s3.Object("artifacts", "jobs/123/output.json")
if _, err := obj.WriteJSON(ctx, map[string]any{"ok": true}, nil); err != nil {
return err
}
body, err := obj.JSON(ctx, nil)
if err != nil {
return err
}
_ = bodyThe same contract exists across Go, Python, Rust, and TypeScript:
- connect to the default binding with
S3()/new S3()/S3::connect().await - connect to a named binding with
S3("archive")or its language equivalent - operate on object handles with
stat,bytes/text/json,write,delete, andpresign
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
plugins:
media:
source: ./plugins/media/manifest.yaml
s3:
- assetsImplementation 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. - Plugin-scoped presigning is disabled by the host transport because signed URLs would expose the internal namespace prefix used to isolate plugin data.
What to read next
- Providers > S3: deployment configuration
- Config File: YAML shape
- Provider Manifests: manifest fields and packaging