feat: lazy S3 bucket provisioning on first upload#969
Merged
pyramation merged 4 commits intomainfrom Apr 9, 2026
Merged
Conversation
When a presigned PUT URL is requested for a database whose S3 bucket doesn't exist yet, the plugin now automatically provisions it with the correct privacy policies, CORS rules, and lifecycle settings. Changes: - Add EnsureBucketProvisioned callback type to plugin options - Add in-memory Set cache (provisionedBuckets) in storage-module-cache to track which S3 buckets are known to exist — no TTL needed since buckets are never deleted; resets on server restart (idempotent) - Wire ensureS3BucketExists into requestUploadUrl before generating the presigned PUT URL — first request provisions, subsequent skip - Add createEnsureBucketProvisioned factory in presigned-url-resolver that uses BucketProvisioner for full bucket setup - Wire into ConstructivePreset with same allowedOrigins as bucket provisioner plugin
Contributor
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
…ardcoding Replace hardcoded ['http://localhost:3000'] with getAllowedOrigins() helper that reads SERVER_ORIGIN from the env/config system. Falls back to localhost for local dev when SERVER_ORIGIN is not set. Also fixes the pre-existing hardcoded value in BucketProvisionerPreset.
The lazy provisioner now follows the same CORS resolution as the bucket provisioner plugin: 1. Per-database allowed_origins from storage_module table 2. Global fallback from SERVER_ORIGIN env var Changes: - Add allowedOrigins to StorageModuleConfig type + SQL query - Add allowedOrigins param to EnsureBucketProvisioned callback - Pass storageConfig.allowedOrigins through ensureS3BucketExists - In createEnsureBucketProvisioned, use per-database origins when available, fall back to getAllowedOrigins() (SERVER_ORIGIN env)
devin-ai-integration bot
pushed a commit
that referenced
this pull request
Apr 10, 2026
Adds a new integration test that exercises the full upload pipeline for both public and private files using real MinIO: - requestUploadUrl → PUT to presigned URL → confirmUpload - Tests public bucket (is_public=true) and private bucket (is_public=false) - Tests content-hash deduplication - Uses lazy S3 bucket provisioning (from PR #969) - Uses per-database bucket naming (from PR #968) Includes seed fixtures (simple-seed-storage) that create: - jwt_private schema with current_database_id() function - metaschema tables (database, schema, table, field) - services tables (apis, domains, api_schemas) - storage_module config row - storage tables (buckets, files, upload_requests) - Two buckets: public and private
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Instead of requiring S3 buckets to exist before any upload can happen, the presigned URL plugin now lazily provisions S3 buckets on the first
requestUploadUrlcall for each database.How it works:
Set<string>(provisionedBuckets) tracks which S3 bucket names have been seen this server processensureS3BucketExists()checks the set → if absent, calls the configuredensureBucketProvisionedcallback → marks the bucket as provisionedcreateEnsureBucketProvisionedinpresigned-url-resolver.ts) usesBucketProvisioner.provision()to create the bucket with correct CORS, privacy policies, etc.provision()is idempotent (handles "already exists")This closes the gap where the SQL seed trigger creates logical bucket rows (public/private/temp) per user but doesn't create actual S3 buckets — those are now created transparently on first upload.
Follows up on PR #968 (per-database bucket support).
CORS origin resolution (per-database)
CORS
allowedOriginsfor lazy provisioning follows a 2-tier hierarchy matching the bucket provisioner plugin pattern:allowed_originsfrom thestorage_moduletable rowSERVER_ORIGINenv var (viagetAllowedOrigins()helper, defaults to['http://localhost:3000']for local dev)The
StorageModuleConfigtype and SQL query now includeallowedOrigins, which is passed throughensureS3BucketExists→ensureBucketProvisioned→BucketProvisioner.provision(). Eachprovision()call receives the resolved origins, so different databases can have different CORS configurations.The pre-existing hardcoded
['http://localhost:3000']inBucketProvisionerPresetwas also replaced withgetAllowedOrigins().Review & Testing Checklist for Human
requestUploadUrlcalls for a new database will both attemptprovision(). The in-memory Set has no mutex, so both will pass theisS3BucketProvisionedcheck. Verify thatBucketProvisioner.provision()truly handles "BucketAlreadyOwnedByYou" / "BucketAlreadyExists" gracefully without throwing.ensureBucketProvisionedthrows (e.g., IAM permissions missing), confirm the error surfaces clearly to the caller and that the bucket is NOT marked as provisioned in the Set (so retries work). The current code callsmarkS3BucketProvisionedafter theawait, so a throw should skip the mark — but verify.allowed_origins: The lazy provisioner readsallowed_originsfrom thestorage_moduletable. Verify that this column is populated correctly for your databases. WhenNULL, falls back toSERVER_ORIGINenv var. Note: the bucket provisioner plugin has a 3-tier hierarchy (bucket → storage_module → global) but the lazy provisioner only uses 2 tiers (storage_module → global) since we're creating a new bucket, not updating an existing one.SERVER_ORIGINenv var: Confirm this is set in production/staging. When unset, falls back to['http://localhost:3000']. The helper wraps the singleserver.originstring into an array — verify this is sufficient if you need multiple origins.requestUploadUrl) against a database that has never had an upload. Confirm the S3 bucket gets created automatically and the presigned URL works. Then upload again and verify no provisioning call is made (check logs for absence of "Lazy-provisioning" message).Notes
@constructive-io/bucket-provisioneras a direct dependency ofgraphile-settings(was already an indirect dep viagraphile-bucket-provisioner-plugin).requestUploadUrlgets lazy provisioning —confirmUploadand download URL generation are unchanged, per request.clearStorageModuleCache()function also clears the provisioned-buckets set, which is useful for testing.Link to Devin session: https://app.devin.ai/sessions/e47513cf8b974ae6985c42c0a657e4d7
Requested by: @pyramation