Skip to content

feat: S3 setup#83

Open
spokonya wants to merge 6 commits intomainfrom
s3-setup
Open

feat: S3 setup#83
spokonya wants to merge 6 commits intomainfrom
s3-setup

Conversation

@spokonya
Copy link
Contributor

@spokonya spokonya commented Jan 27, 2026

Description

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactoring (code improvement without changing functionality)
  • Documentation update
  • Configuration/infrastructure change
  • Performance improvement
  • Test coverage improvement

Related Issue(s)

Closes #95
Working towards #51

What Changed?

  • Added S3 configuration (config/s3.go) with environment variable support for AWS credentials, region, and bucket name
    -Added S3 test handler
  • Integrated S3 storage into app initialization and route setup (server.go)
  • Made an s3 storage layer that initializes S3 client with credentials, generates time-limited upload URLs for direct client-to-S3 uploads, and allows for server-side file deletion from S3

Testing & Validation

How this was tested

  1. started local server with s3 credentials configured
  2. Created a call for a presigned URL
  3. Used curl -X PUT with the presigned URL to upload a test file directly to s3
  4. verified file appeared in my s3 dashboard

Screenshots/Recordings

Unfinished Work & Known Issues

  • None, this PR is complete and production-ready
  • The following items are intentionally deferred:
    • user profile picture endpoints
    • Frontend UI for uploads

Notes & Nuances



Pre-Merge Checklist

Code Quality

  • Code follows the project's style guidelines and conventions
  • Self-review completed (I've reviewed my own code for obvious issues)
  • No debugging code, console logs, or commented-out code left behind
  • No merge conflicts with the base branch
  • Meaningful commit messages that explain the "why"

Testing & CI

  • All CI checks are passing
  • All new and existing tests pass locally
  • Test coverage hasn't decreased (or decrease is justified)
  • Linting passes without errors

Documentation

  • Code is self-documenting or includes helpful comments for complex logic
  • API documentation updated (if backend endpoints changed)
  • Type definitions are accurate and up-to-date

Reviewer Notes

  • Areas needing extra attention: ...
  • Questions for reviewers: I need help with my .env variables. When I was testing I connected to local supabase but I couldn't get connecting to the remote server. It was either telling me that I was missing a required value (which I had) or that "Tenant or user not found"

@spokonya spokonya requested review from Dao-Ho and danctila January 30, 2026 15:42
@danctila danctila linked an issue Jan 31, 2026 that may be closed by this pull request
@danctila danctila changed the title S3 setup feat: S3 setup Feb 1, 2026
Copy link
Contributor

@danctila danctila left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice implementation @spokonya 🔥🔥🔥

Some good things to note

  • presigned URL usage is great
  • good separation of concerns b/t config, storage and handler
  • like the swagger docs 🙏
  • context propagation is correct

Must fix

  • Missing an update to the .env.sample
    • Make sure to update this as the ticket mentioned "you should probably update env.sample if you've updated your .env" just so it's easier to keep track over time for both engs and the people who will look at this code after us
  • Swagger import commented out
  • File location for the S3 storage file

Other suggestions are related to error context, input validation, and nits about code quality/cleanup but overall nothing major to change ✌️


"github.com/generate/selfserve/config"
_ "github.com/generate/selfserve/docs"
//_ "github.com/generate/selfserve/docs"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure what happened here but i think this breaks swagger docs

Either uncomment the line or describe why it was removed if it was deliberate (might just be for debugging?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uncommented

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the S3 storage location being under storage/postgres/s3/ might not make the most sense architecturally because S3 is not related to Postgres

I would recommend:

backend/internal/service/storage/
|-postgres/
|  |----storage.go
|
|-s3/
   |---s3storage.go

Make sure to:

  • move the file
  • update all imports necessary

Comment on lines +54 to +56
if err != nil {
return "", err
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shoudn't return raw errors without context - will make debugging more difficult, etc. so we want to follow our err wrapping setup

suggestion for example:

Suggested change
if err != nil {
return "", err
}
if err != nil {
return "", fmt.Errorf("failed to generate presigned URL for key %s: %w", key, err)
}

Also apply similar wrapping to:

  • DeleteFile
  • NewS3Storage

Comment on lines +44 to +50
func (s *Storage) GeneratePresignedURL(ctx context.Context, key string, expiration time.Duration) (string, error) {

presignedURL, err := s.URL.PresignPutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(s.BucketName),
Key: aws.String(key),
}, func(opts *s3.PresignOptions) {
opts.Expires = expiration
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sure the functions have input validation

func (s *Storage) GeneratePresignedURL(ctx context.Context, key string, expiration time.Duration) (string, error) {
	if key == "" {
		return "", fmt.Errorf("key cannot be empty")
	}
	if expiration <= 0 {
		return "", fmt.Errorf("expiration must be positive")
	}

Also do the same for:

  • DeleteFile (validating that key is not empty)

return errs.BadRequest("key is required")
}

presignedURL, err := h.S3Storage.GeneratePresignedURL(c.Context(), key, 5*time.Minute)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be good to have the 5-minute hardcoded expiration either configurable via environment variable or defined as a package level constant.

for example:

const DefaultPresignedURLExpiration = 5 * time.Minute
...
presignedURL, err := h.S3Storage.GeneratePresignedURL(c.Context(), key, DefaultPresignedURLExpiration)

}

func (s *Storage) GeneratePresignedURL(ctx context.Context, key string, expiration time.Duration) (string, error) {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

return c.JSON(fiber.Map{
"presigned_url": presignedURL,
})
} No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go convention to end files with newline character

}, nil
}

func (s *Storage) GeneratePresignedURL(ctx context.Context, key string, expiration time.Duration) (string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider adding a contentType parameter to your function.

we want to make the s3 implementation as scalable as possible beyond profile pictures. for profile picture we will want to restrict file types to types like image/jpeg or image/png for example but other uses of s3 won't have the same restrictions

URL *s3.PresignClient
}

func NewS3Storage(cfg config.S3) (*Storage, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have upload (presigned PUT) and delete but looks like no way to retrieve files? we may want a GetFile URL generator similar to your other functions like:

func (s *Storage) GenerateGetURL(ctx context.Context, key string, expiration time.Duration) (string, error) {
	presignedURL, err := s.URL.PresignGetObject(ctx, &s3.GetObjectInput{
		Bucket: aws.String(s.BucketName),
		Key:    aws.String(key),
	}, func(opts *s3.PresignOptions) {
		opts.Expires = expiration
	})
...
}

// @Accept json
// @Produce json
// @Param key path string true "File key"
// @Success 200 {string} string "Presigned URL"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

swagger docs here show @success 200 {string} string but the actual response is a JSON object so it should be

Suggested change
// @Success 200 {string} string "Presigned URL"
// @Success 200 {object} map[string]string "Presigned URL response"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: initial s3 setup feat: profile picture

2 participants

Comments