feat(storefront): redisStorage.fromEnv() — storage from standard deployment env (#54) - #128
feat(storefront): redisStorage.fromEnv() — storage from standard deployment env (#54)#128dzuluaga wants to merge 4 commits into
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 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".
| const url = firstSet(env, ENV_URL); | ||
| const token = firstSet(env, ENV_TOKEN); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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>
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
redisStorage.fromEnv()to use it.{ required: true }, which throws a clear error (naming the exact variables to set) instead of falling back.How to test
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 customnamespacethrough 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 tocreateStorefront: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:
redisStorage.fromEnv()(a sibling constructor)storage: "env"(magic string oncreateStorefront)redisStorage.fromEnv()wins on the rubric:redisStorage.fromEnv(), so it's plain that Redis is being built from env. A barestorage: "env"hides both which backend and which variables, and puts a magic string into a slot typed as aStorageProviderobject./redissubmodule (which owns the optional@upstash/redispeer dependency).storage: "env"would forcecreateStorefront(the coreserver.ts, which today has zero Redis knowledge) to reach into Redis, coupling the core to an optional dependency.redisStorage(options)and reads likeArray.from/Buffer.from;createStorefrontis untouched, no new required argument anywhere.StorageProvider | undefined, andundefinedis the meaningful value thestorage?: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:
Why the example file is unchanged (follow-up)
The quickstart runs against the published npm packages, not this working tree (
examples/quickstart/package.jsondepends on@openmobilehub/credentagent-storefront@^0.3.1, andexamples/is not an npm workspace). The published 0.3.1 has nofromEnv, so swapping the example now crashes the smoke test on startup. I verified this directly: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— addsRedisStorageFromEnvOptionsand theredisStorage.fromEnv()static (function + namespace merge). ReadsKV_REST_API_URL/KV_REST_API_TOKENthenUPSTASH_REDIS_REST_URL/UPSTASH_REDIS_REST_TOKENper field, matching the quickstart's precedence exactly. Env source is injectable (_env) for tests.packages/credentagent-storefront/src/redis.test.ts— newfromEnvsuite (8 tests), including a load-bearing precedence test (flip the order and it fails) that constructs the lazy client through the existing_loadseam to assert exactly which variables were read.packages/credentagent-storefront/README.md— documentsredisStorage.fromEnv()in the "Production persistence" section (the published doc).Closes #54.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Y5Y1aX9ffAF26hP4fgQFi7