Skip to content

feat(storefront): redisStorage.fromEnv() — storage from standard deployment env (#54) - #128

Open
dzuluaga wants to merge 4 commits into
mainfrom
dx/54-storage-from-env
Open

feat(storefront): redisStorage.fromEnv() — storage from standard deployment env (#54)#128
dzuluaga wants to merge 4 commits into
mainfrom
dx/54-storage-from-env

Conversation

@dzuluaga

Copy link
Copy Markdown
Contributor

In plain terms

When you run this shopping server on a real host (like Vercel), it needs a shared database so that a shopping cart started on one machine is still there when the checkout lands on another. That database is a hosted Redis, and the host hands you its address and password as environment variables. Until now the example had to read those variables by hand and stitch them together before passing them in. This adds a one-liner — redisStorage.fromEnv() — that reads those standard variables for you. If they aren't set (like on your laptop), it quietly returns "nothing," which the server already understands as "just use in-memory storage." So the same file works locally with no setup and switches to the shared database automatically once it's deployed.

What you're approving

  • A new, optional helper on an existing package. Nothing that already works changes, and nothing new ships to users by default — you have to call redisStorage.fromEnv() to use it.
  • The two supported hosting setups are read in a fixed order: Vercel KV first, then Upstash (both are hosted Redis services). This matches exactly what the quickstart example does today by hand.
  • Worst case if it's wrong: a deployment that meant to use the shared database silently uses in-memory storage instead. To guard against that, callers who must persist can pass { required: true }, which throws a clear error (naming the exact variables to set) instead of falling back.
  • The example file is deliberately not changed in this PR — see the note below.

How to test

npm ci
npm run build
npm run -w @openmobilehub/credentagent-storefront test   # 106 pass, incl. the new fromEnv suite
npm run lint

The new tests cover: reads the Vercel KV pair, reads the Upstash pair, prefers Vercel KV when both are present, returns nothing when the variables are absent (and when only half a pair is set), throws a clear error when { required: true } and nothing is set, and passes a custom namespace through to the stored keys.


For reviewers — the detail

Why (issue #54)

Issue #54 came out of spec 007 research note R5: the quickstart hero file (examples/quickstart/server.mjs) hand-builds the Redis connection from environment variables (~2 lines) before passing it to createStorefront:

const kv = { url: process.env.KV_REST_API_URL ?? process.env.UPSTASH_REDIS_REST_URL, token: process.env.KV_REST_API_TOKEN ?? process.env.UPSTASH_REDIS_REST_TOKEN };
// ...
storage: kv.url && kv.token ? redisStorage(kv) : undefined,

Per Architecture Principle I / P12 ("the example IS the DX test — fix the API, not the example", docs/reference/architecture-principles.md), that plumbing is a signal the library should absorb it.

API shape chosen — and why

I weighed the three candidates from the issue against the DX rubric:

Candidate Verdict
redisStorage.fromEnv() (a sibling constructor) Chosen.
storage: "env" (magic string on createStorefront) Rejected — see below.
auto-detect-when-deployed (default-on) Rejected — hidden magic (P11); the issue itself flags this.

redisStorage.fromEnv() wins on the rubric:

  • No hidden magic / origin visible (P11). The caller writes redisStorage.fromEnv(), so it's plain that Redis is being built from env. A bare storage: "env" hides both which backend and which variables, and puts a magic string into a slot typed as a StorageProvider object.
  • Ports, not imports (P6). All Redis/env knowledge stays inside the /redis submodule (which owns the optional @upstash/redis peer dependency). storage: "env" would force createStorefront (the core server.ts, which today has zero Redis knowledge) to reach into Redis, coupling the core to an optional dependency.
  • Consistency (P2) and additive (P1/P9). It mirrors the existing redisStorage(options) and reads like Array.from / Buffer.from; createStorefront is untouched, no new required argument anywhere.
  • One clean return, not a maybe to re-check (P3/P4). It returns StorageProvider | undefined, and undefined is the meaningful value the storage?: option already accepts (the in-memory default) — the caller passes it straight through with no re-check. For callers who must persist, { required: true } is the "throw" door.

Before / after in the example's storage line:

// before (2 lines: a `const kv = {…}` line + the conditional)
const kv = { url: process.env.KV_REST_API_URL ?? process.env.UPSTASH_REDIS_REST_URL, token: process.env.KV_REST_API_TOKEN ?? process.env.UPSTASH_REDIS_REST_TOKEN };
storage: kv.url && kv.token ? redisStorage(kv) : undefined,

// after (the whole block collapses to)
storage: redisStorage.fromEnv(),

Why the example file is unchanged (follow-up)

The quickstart runs against the published npm packages, not this working tree (examples/quickstart/package.json depends on @openmobilehub/credentagent-storefront@^0.3.1, and examples/ is not an npm workspace). The published 0.3.1 has no fromEnv, so swapping the example now crashes the smoke test on startup. I verified this directly:

# with the example switched to redisStorage.fromEnv():
$ npm run smoke
TypeError: redisStorage.fromEnv is not a function
server never came up

So the example is left as-is and is safe to merge ahead of a release. The one-line swap above should land in a follow-up PR after the next npm publish (the new code is semantically identical to the current hand-wiring, so behavior is unchanged).

Files

  • packages/credentagent-storefront/src/redis.ts — adds RedisStorageFromEnvOptions and the redisStorage.fromEnv() static (function + namespace merge). Reads KV_REST_API_URL/KV_REST_API_TOKEN then UPSTASH_REDIS_REST_URL/UPSTASH_REDIS_REST_TOKEN per field, matching the quickstart's precedence exactly. Env source is injectable (_env) for tests.
  • packages/credentagent-storefront/src/redis.test.ts — new fromEnv suite (8 tests), including a load-bearing precedence test (flip the order and it fails) that constructs the lazy client through the existing _load seam to assert exactly which variables were read.
  • packages/credentagent-storefront/README.md — documents redisStorage.fromEnv() in the "Production persistence" section (the published doc).

Closes #54.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y5Y1aX9ffAF26hP4fgQFi7

…oyment env

Add a first-class `redisStorage.fromEnv()` so a deployment reads its Redis
connection straight from the env it already sets — Vercel KV
(`KV_REST_API_URL`/`KV_REST_API_TOKEN`) or Upstash
(`UPSTASH_REDIS_REST_URL`/`UPSTASH_REDIS_REST_TOKEN`), in that precedence —
instead of hand-wiring `redisStorage({ url, token })`.

It returns a provider, or `undefined` when no env pair is set, which is exactly
`storage: undefined` (the in-memory zero-config default) — so the same file runs
locally with no env and persists on a deployment with no code change. Pass
`{ required: true }` to throw (naming the vars) instead of degrading, and
`{ namespace }` to isolate keys on a shared backend.

The quickstart example is intentionally left unchanged: it runs against the
PUBLISHED package, which won't expose `fromEnv` until the next npm publish
(verified — switching the example crashes `npm run smoke` with "redisStorage.fromEnv
is not a function"). The example swap is tracked as a follow-up in the PR.

Closes #54.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5Y1aX9ffAF26hP4fgQFi7
Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
@vercel

vercel Bot commented Jul 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
credentagent-demo Ready Ready Preview, Comment Jul 28, 2026 6:56am

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9fb3c2fe58

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +221 to +222
const url = firstSet(env, ENV_URL);
const token = firstSet(env, ENV_TOKEN);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Select the URL and token from the same credential pair

When one higher-precedence variable is stale or only partially configured alongside a complete lower-precedence pair—for example, KV_REST_API_URL plus both Upstash variables—these independent lookups combine the Vercel URL with the Upstash token. The helper then returns a provider instead of selecting the complete Upstash pair (or rejecting the configuration), so Redis operations fail and may send a credential to the wrong endpoint. Evaluate each documented URL/token pair atomically before applying precedence.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 58fb56f. fromEnv() now selects a COMPLETE pair atomically — the first of Vercel KV then Upstash where BOTH of that provider's vars are set — so a stale/partial KV url is never combined with an Upstash token (and vice-versa); a cross-provider mix yields undefined (or a clearer required error). Added a mixed-env test (KV url + complete Upstash pair -> selects Upstash atomically; fails under independent lookups) and a cross-provider-halves test (-> undefined). The precedence test still holds.

…er mix providers)

Codex P2: fromEnv() picked url and token independently, so KV_REST_API_URL plus only
the Upstash pair produced a Vercel url with an Upstash token — Redis ops fail, and a
credential could be sent to the wrong endpoint. Now the url and token are chosen from
the SAME provider pair: the first COMPLETE pair wins (Vercel KV, then Upstash), else
undefined (or a clearer `required` error). A cross-provider mix is ignored, never
combined.

Tests: the precedence test stays; added a mixed-env case (stale KV url + complete
Upstash pair → selects Upstash atomically; fails if url/token are picked independently)
and a cross-provider-halves case (→ undefined).

Storefront suite 108, build + lint green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5Y1aX9ffAF26hP4fgQFi7
Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
Signed-off-by: Diego Zuluaga <dfzuluaga@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DX: storefront storage-from-standard-env convenience (drop the example's Redis plumbing)

1 participant