---
techId: s3
category: storage
---

# AWS S3

> **General guidance only.** Check `<project>/.seeflow/WIKI.md`
> `## Tech stack adaptations` first — project-specific helpers,
> fixtures, and conventions always win over the templates below.
> Whatever you learn this run, append back into that section so the
> next flow reuses it.

## Node modelling

- One `stateNode` per bucket, not per key or prefix.
- Duplicate the bucket node next to each consumer when it makes the
  flow easier to read (same `kind` + `name`, unique `id`).
- Treat S3 as long-lived `state`, not `event`.

## Play (trigger locally)

- Reuse a project uploader helper over a raw SDK client when one exists.
- Honour `AWS_ENDPOINT_URL` / `S3_ENDPOINT` so LocalStack or MinIO works.
- For local emulators force `UsePathStyle = true` — virtual-hosted URLs
  break against `localhost`.

```go
package main

import (
	"bytes"; "context"; "time"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/aws/aws-sdk-go-v2/aws"
)

func main() {
	cfg, _ := config.LoadDefaultConfig(context.Background())
	c := s3.NewFromConfig(cfg, func(o *s3.Options){ o.UsePathStyle = true })
	key := time.Now().UTC().Format("20060102T150405") + ".json"
	_, _ = c.PutObject(context.Background(), &s3.PutObjectInput{
		Bucket: aws.String("orders"), Key: aws.String(key),
		Body: bytes.NewReader([]byte(`{"id":"o_1","total":42}`)),
	})
	println("uploaded", key)
}
```

## Status (read locally)

- `ListObjectsV2` with `Prefix` + `MaxKeys` — never full-bucket.
- Emit `StatusReport` per tick.
- Missing bucket → `state: "warn"`, not panic.

```go
package main

import (
	"context"; "encoding/json"; "fmt"; "time"
	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/s3"
)

func main() {
	cfg, _ := config.LoadDefaultConfig(context.Background())
	c := s3.NewFromConfig(cfg, func(o *s3.Options){ o.UsePathStyle = true })
	for {
		out, err := c.ListObjectsV2(context.Background(), &s3.ListObjectsV2Input{Bucket: aws.String("orders"), MaxKeys: aws.Int32(20)})
		state := "ok"; if err != nil { state = "warn" }
		keys := []string{}; for _, o := range out.Contents { keys = append(keys, *o.Key) }
		b, _ := json.Marshal(map[string]any{"state":state,"summary":fmt.Sprintf("%d objects", len(keys)),"data":keys,"ts":time.Now().Unix()})
		println(string(b))
		time.Sleep(2 * time.Second)
	}
}
```

## Gotchas

- Buckets are region-scoped; SDK silently uses `AWS_REGION` and 301s
  cross-region — pin the region per script.
- LocalStack/MinIO require `UsePathStyle=true`; without it the SDK
  builds `bucket.localhost` URLs that don't resolve.
- `AWS_ENDPOINT_URL_S3` overrides `AWS_ENDPOINT_URL` in newer SDKs.

## Fixture shape

```json
{ "bucket": "orders", "key": "20260520T120000.json", "size": 24, "etag": "\"d41d...\"", "lastModified": "2026-05-20T12:00:00Z" }
```
