Dentolize · Dynamic Clinic Seeder Walkthrough
On this pageBusiness viewTechnical view

S3-compatible uploads

Business view

Dentolize stores uploaded files (images, documents) in Amazon S3 in production. The sandbox, however, runs on self-hosted infrastructure and uses MinIO — a storage service that speaks the same API as S3 but lives on the homelab, not AWS.

For file uploads to work in a sandbox, the app has to be able to point at MinIO instead of AWS. This PR adds that ability through configuration, so:

  • In the sandbox, uploads go to MinIO.
  • In production, with the new settings left unset, nothing changes — uploads

go to AWS S3 exactly as before.

This is the one change in the PR that touches a code path production actually runs, so it was kept deliberately backward-compatible.

Technical view

The S3 client · s3.js:15-33

Two optional env vars were added to the client factory:

const endpoint       = process.env.S3_ENDPOINT                  // s3.js:15
const forcePathStyle = process.env.S3_FORCE_PATH_STYLE === 'true' // s3.js:16
…
region: region || 'us-east-1',
...(endpoint ? { endpoint, forcePathStyle } : {})               // s3.js:33
  • When S3_ENDPOINT is unset (production), the spread contributes nothing and

the SDK targets real AWS S3 — identical to before.

  • When set (sandbox), the client targets MinIO. S3_FORCE_PATH_STYLE=true is

required because MinIO doesn't support virtual-hosted-style bucket subdomains (s3.js:11-14 comment).

A single source of truth for object URLs · s3.js:60-63

export const s3ObjectUrl = key => {
  const base = process.env.S3_PUBLIC_BASE_URL
  if (base) return `${base.replace(/\/+$/, '')}/${key}`               // MinIO / self-hosted
  return `https://${process.env.S3_BUCKET_AWS}.s3.${process.env.S3_REGION_AWS}.amazonaws.com/${key}` // AWS
}

This is the single source of truth for the public URL of an uploaded object. With S3_PUBLIC_BASE_URL set, it builds a MinIO URL; unset, it builds the standard AWS virtual-hosted URL. Note the AWS branch now reads S3_BUCKET_AWS and S3_REGION_AWS from the environment rather than hardcoding the bucket.

The upload resolver · authMutations.js:1169-1198

The presigned-upload mutation was updated to use both:

  • Bucket from env, with the historical production bucket as fallback:

const bucket = process.env.S3_BUCKET_AWS || 'dentolize' (authMutations.js:1169), used for both the presigned Bucket and the policy Conditions (authMutations.js:1171 and the { bucket } condition).

must match what the readers of the file: redis keys and deleteS3Object reconstruct — hence "single source of truth."

The redis dedup key and the returned url are both derived from s3ObjectUrl now, replacing the previous signedRequest.url + fields.key construction (authMutations.js:1191-1199). Functionally: in production (env unset) the URL shape is unchanged; in the sandbox it correctly points at MinIO.

Why it's low-risk

Every new behavior is gated on an env var that is unset in production. The || 'dentolize' fallback preserves the historical bucket name. So the blast radius is "sandbox uploads now work" with no change to the production upload path.

Support tip. If sandbox uploads fail, suspect the environment config (S3_ENDPOINT, S3_BUCKET_AWS, S3_PUBLIC_BASE_URL, S3_FORCE_PATH_STYLE), not the app code. See For Support.