Skip to content

Commit f5fa1ed

Browse files
committed
feat(storage): resolve S3 config from S3_* env vars at runtime
astro.config.mjs is evaluated in Node at Astro build time, so values passed to s3({...}) are captured and baked into the virtual emdash/config module. Operators running the same image across multiple container deployments (per-tenant credentials injected at boot) have no way to change those values without a rebuild. Adds runtime resolution: any field omitted from s3({...}) is read from the matching S3_* env var when the container starts. Existing deployments that pass explicit values to s3({...}) are unaffected. Env var names match the existing docs convention (S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_REGION, S3_PUBLIC_URL). Precedence per field: explicit s3({...}) value > S3_* env var > absent. Empty strings are treated as absent in both sources. Endpoint values are validated as http/https URLs with a host at both sources; invalid values throw MISSING_S3_CONFIG with the source named so operators know whether to fix their env var or their astro.config.mjs. endpoint and bucket remain required. accessKeyId and secretAccessKey become optional in the type (both or neither). Relaxes s3() to accept Partial<S3StorageConfig> so s3() with no args is valid for fully env-driven deployments. Scope: Node. process.env is used for runtime reads, guarded with typeof-process for bundler safety. Workers users should continue passing explicit values to s3({...}); worker binding resolution is not in scope for this change.
1 parent cf72f4a commit f5fa1ed

File tree

8 files changed

+507
-59
lines changed

8 files changed

+507
-59
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"emdash": minor
3+
---
4+
5+
Adds runtime resolution of S3 storage config from `S3_*` environment
6+
variables (`S3_ENDPOINT`, `S3_BUCKET`, `S3_ACCESS_KEY_ID`,
7+
`S3_SECRET_ACCESS_KEY`, `S3_REGION`, `S3_PUBLIC_URL`). Any field omitted from
8+
`s3({...})` is read from the matching env var on Node at runtime, so
9+
container images can be built once and receive credentials at boot without a
10+
rebuild. Explicit values in `s3({...})` still take precedence.
11+
12+
`s3()` with no arguments is now valid for fully env-driven deployments.
13+
`accessKeyId` and `secretAccessKey` are now optional in `S3StorageConfig`
14+
(both or neither). Workers users should continue passing explicit values to
15+
`s3({...})`.

docs/src/content/docs/deployment/storage.mdx

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,21 @@ storage: r2({
9393

9494
The S3 adapter works with Cloudflare R2 (via S3 API), MinIO, and other S3-compatible services.
9595

96+
<Aside type="caution" title="Install the AWS SDK first">
97+
EmDash uses the AWS SDK at runtime for the S3 adapter but does not bundle it — core is
98+
deliberately SDK-agnostic so R2-only and local-only deployments stay lean. Install the SDK
99+
in your project before using `s3()`:
100+
101+
```sh
102+
pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
103+
```
104+
105+
If you skip this step, `astro build` will fail with `Rollup failed to resolve import
106+
"@aws-sdk/client-s3"`. The R2 binding adapter (`r2()`) and local adapter do not need the
107+
SDK and are unaffected.
108+
109+
</Aside>
110+
96111
```js title="astro.config.mjs"
97112
import emdash, { s3 } from "emdash/astro";
98113

@@ -114,14 +129,66 @@ export default defineConfig({
114129

115130
### Configuration
116131

117-
| Option | Type | Description |
118-
| ----------------- | -------- | ------------------------------ |
119-
| `endpoint` | `string` | S3 endpoint URL |
120-
| `bucket` | `string` | Bucket name |
121-
| `accessKeyId` | `string` | Access key |
122-
| `secretAccessKey` | `string` | Secret key |
123-
| `region` | `string` | Region (default: `"auto"`) |
124-
| `publicUrl` | `string` | Optional CDN or public URL |
132+
| Option | Type | Required | Description |
133+
| ----------------- | -------- | -------- | ------------------------------ |
134+
| `endpoint` | `string` | yes | S3 endpoint URL |
135+
| `bucket` | `string` | yes | Bucket name |
136+
| `accessKeyId` | `string` | no\* | Access key |
137+
| `secretAccessKey` | `string` | no\* | Secret key |
138+
| `region` | `string` | no | Region (default: `"auto"`) |
139+
| `publicUrl` | `string` | no | Optional CDN or public URL |
140+
141+
\* Both `accessKeyId` and `secretAccessKey` must be provided together, or both omitted.
142+
143+
### Resolving S3 config from environment variables
144+
145+
Any field omitted from `s3({...})` is read from the matching `S3_*` environment variable
146+
when the process starts. This lets you build a container image once and inject credentials
147+
at boot without a rebuild. Explicit values in `s3({...})` always take precedence over
148+
environment variables.
149+
150+
| Environment variable | Field | Notes |
151+
| ---------------------- | ----------------- | ---------------------------------- |
152+
| `S3_ENDPOINT` | `endpoint` | Must be a valid `http`/`https` URL |
153+
| `S3_BUCKET` | `bucket` | |
154+
| `S3_ACCESS_KEY_ID` | `accessKeyId` | |
155+
| `S3_SECRET_ACCESS_KEY` | `secretAccessKey` | |
156+
| `S3_REGION` | `region` | Defaults to `"auto"` |
157+
| `S3_PUBLIC_URL` | `publicUrl` | Optional CDN prefix |
158+
159+
Environment variables are read from `process.env` when the process starts. This is a
160+
Node-only feature.
161+
162+
```js title="astro.config.mjs — runtime environment variable example"
163+
import emdash, { s3 } from "emdash/astro";
164+
165+
export default defineConfig({
166+
integrations: [
167+
emdash({
168+
// s3() with no args: all fields from S3_* environment variables
169+
storage: s3(),
170+
171+
// Or mix: override one field, rest from environment
172+
// storage: s3({ publicUrl: "https://cdn.example.com" }),
173+
}),
174+
],
175+
});
176+
```
177+
178+
<Aside type="note" title="Cloudflare Workers">
179+
This runtime resolution does not work on Cloudflare Workers. Worker secrets and variables
180+
are exposed through the `env` parameter of the fetch handler (or `import { env } from
181+
"cloudflare:workers"`), not through `process.env` — even with the `nodejs_compat` flag
182+
enabled. For Workers deployments:
183+
184+
- If you are using R2, use the [`r2()` adapter](#cloudflare-r2-binding) from
185+
`@emdash-cms/cloudflare`. This is the recommended path.
186+
- If you need a non-R2 S3-compatible backend on Workers, pass explicit values to
187+
`s3({...})` in `astro.config.mjs`. Note that `astro.config.mjs` runs at build time,
188+
so Worker secrets are not directly accessible there; you will need to inject values
189+
through your build pipeline.
190+
191+
</Aside>
125192

126193
### R2 via S3 API
127194

docs/src/content/docs/reference/configuration.mdx

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ storage: r2({
7272
publicUrl: "https://pub-xxxx.r2.dev", // optional
7373
});
7474

75-
// S3-compatible (any platform)
75+
// S3-compatible (any platform) — all fields from S3_* environment variables
76+
storage: s3()
77+
78+
// Or with explicit values
7679
storage: s3({
7780
endpoint: "https://s3.amazonaws.com",
7881
bucket: "my-bucket",
@@ -387,29 +390,50 @@ r2({
387390
});
388391
```
389392

390-
### `s3(config)`
393+
### `s3(config?)`
394+
395+
S3-compatible storage. All config fields are optional: any field omitted from
396+
`s3({...})` is resolved from the matching `S3_*` environment variable when the
397+
Node process starts. Explicit values always take precedence.
391398

392-
S3-compatible storage.
399+
**Prerequisite:** install `@aws-sdk/client-s3` and `@aws-sdk/s3-request-presigner`
400+
in your project. EmDash core does not bundle the AWS SDK. See
401+
[Storage Options → S3-Compatible Storage](/deployment/storage/#s3-compatible-storage)
402+
for details.
393403

394-
| Option | Type | Description |
395-
| ----------------- | -------- | -------------------------- |
396-
| `endpoint` | `string` | S3 endpoint URL |
397-
| `bucket` | `string` | Bucket name |
398-
| `accessKeyId` | `string` | Access key |
399-
| `secretAccessKey` | `string` | Secret key |
400-
| `region` | `string` | Region (default: `"auto"`) |
401-
| `publicUrl` | `string` | Optional CDN URL |
404+
| Option | Type | Description |
405+
| ----------------- | -------- | ----------------------------------- |
406+
| `endpoint` | `string` | S3 endpoint URL (`S3_ENDPOINT`) |
407+
| `bucket` | `string` | Bucket name (`S3_BUCKET`) |
408+
| `accessKeyId` | `string` | Access key (`S3_ACCESS_KEY_ID`) |
409+
| `secretAccessKey` | `string` | Secret key (`S3_SECRET_ACCESS_KEY`) |
410+
| `region` | `string` | Region, default `"auto"` (`S3_REGION`) |
411+
| `publicUrl` | `string` | Optional CDN URL (`S3_PUBLIC_URL`) |
402412

403413
```js
414+
// All fields from S3_* environment variables (Node container deployments)
415+
s3()
416+
417+
// Mix: CDN from config, rest from environment
418+
s3({ publicUrl: "https://cdn.example.com" })
419+
420+
// All explicit (unchanged from before)
404421
s3({
405422
endpoint: "https://xxx.r2.cloudflarestorage.com",
406423
bucket: "media",
407424
accessKeyId: process.env.R2_ACCESS_KEY_ID,
408425
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
409426
publicUrl: "https://cdn.example.com",
410-
});
427+
})
411428
```
412429

430+
Runtime environment variable resolution is a Node-only feature. On Cloudflare
431+
Workers, secrets and variables are exposed through the `env` parameter of the
432+
fetch handler, not through `process.env`, so `S3_*` environment variables are
433+
not picked up. Workers deployments should either use the [`r2(config)`](#r2config)
434+
adapter or pass explicit values to `s3({...})`. See
435+
[Storage Options](/deployment/storage/#s3-compatible-storage) for details.
436+
413437
## Live Collections
414438

415439
Configure the EmDash loader in `src/live.config.ts`:

packages/core/src/astro/storage/adapters.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,34 @@ import type { StorageDescriptor, S3StorageConfig, LocalStorageConfig } from "./t
3232
/**
3333
* S3-compatible storage adapter
3434
*
35-
* Works with AWS S3, Cloudflare R2 (via S3 API), Minio, etc.
35+
* Works with AWS S3, Cloudflare R2 (via S3 API), MinIO, etc.
36+
*
37+
* Any field omitted here is resolved from the matching `S3_*` environment
38+
* variable when the container starts (`S3_ENDPOINT`, `S3_BUCKET`,
39+
* `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_REGION`, `S3_PUBLIC_URL`).
40+
* Explicit values always take precedence over env vars.
41+
*
42+
* Note: env var resolution reads `process.env` on Node at runtime.
43+
* Workers users should continue passing explicit values to `s3({...})`.
3644
*
3745
* @example
3846
* ```ts
47+
* // All fields from env (container deployments)
48+
* storage: s3()
49+
*
50+
* // Mix: CDN from config, credentials from env
51+
* storage: s3({ publicUrl: "https://cdn.example.com" })
52+
*
53+
* // All explicit (unchanged from before)
3954
* storage: s3({
4055
* endpoint: "https://xxx.r2.cloudflarestorage.com",
4156
* bucket: "media",
42-
* accessKeyId: process.env.R2_ACCESS_KEY_ID!,
43-
* secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
44-
* publicUrl: "https://cdn.example.com", // optional CDN
57+
* accessKeyId: process.env.R2_ACCESS_KEY_ID,
58+
* secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
4559
* })
4660
* ```
4761
*/
48-
export function s3(config: S3StorageConfig): StorageDescriptor {
62+
export function s3(config: Partial<S3StorageConfig> = {}): StorageDescriptor {
4963
return {
5064
entrypoint: "emdash/storage/s3",
5165
config,

packages/core/src/astro/storage/types.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,18 @@ export interface S3StorageConfig {
3939
endpoint: string;
4040
/** Bucket name */
4141
bucket: string;
42-
/** Access key ID */
43-
accessKeyId: string;
44-
/** Secret access key */
45-
secretAccessKey: string;
42+
/**
43+
* Access key ID.
44+
* May be resolved from the `S3_ACCESS_KEY_ID` env var at runtime on Node.
45+
* Must be provided together with `secretAccessKey`, or both omitted.
46+
*/
47+
accessKeyId?: string;
48+
/**
49+
* Secret access key.
50+
* May be resolved from the `S3_SECRET_ACCESS_KEY` env var at runtime on Node.
51+
* Must be provided together with `accessKeyId`, or both omitted.
52+
*/
53+
secretAccessKey?: string;
4654
/** Optional region (defaults to "auto") */
4755
region?: string;
4856
/** Optional public URL prefix for CDN */

0 commit comments

Comments
 (0)