Skip to Content

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.json

Provider interface

Implement the S3 gRPC service:

MethodPurpose
HeadObjectReturn metadata for one object reference.
ReadObjectStream one object: metadata frame first, then byte frames.
WriteObjectAccept one metadata frame followed by byte frames, then return committed metadata.
DeleteObjectDelete one object reference.
ListObjectsList objects by bucket, prefix, delimiter, and continuation token.
CopyObjectCopy an object server-side between object references.
PresignObjectProduce a presigned request URL plus required headers.
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 } _ = body

The 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, and presign

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: - assets

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, 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.
  • Plugin-scoped presigning is disabled by the host transport because signed URLs would expose the internal namespace prefix used to isolate plugin data.