Repository navigation
Choose the storefront stack #1
Description
Choose the storefront stack: language, runtime, framework, linters, code quality and security checks, and its Docker container. Part of the tech stack epic in commerce.
Status
| Stage | Track | Next action | Branch | PR |
|---|---|---|---|---|
| done | full | Write the stack ADR through a PR | — | — |
Appetite: 3h active (Default) · Estimate: — · Active so far: see the time comment
Brief
Idea: "Determine the best tech stack for the storefront. I prefer TypeScript and Vue in some form (Vue, Nuxt, Ionic etc.). Also linters, code quality and security checks. Each service will be in a Docker container."
Problem and audience: The storefront is the primary application, and it has no stack yet. It needs:
- pages that search engines can index;
- interactive parts: the cart, the account, live updates;
- data fetched through the generated SDK;
- a container that serves it.
Outcome: A recorded choice of rendering framework, data layer, SEO, performance and accessibility tooling, security headers and serving container. Every requirement below is met, or its gap is named.
Scope.
- In: Vue-based options: Nuxt, Astro with Vue islands, and Vite SSR.
- Out: Ionic and a native app (web only, Default). Components and tokens, which come from the design system.
- Deferred: an ADR. The shared toolchain and CI security checks come from the epic, commerce#3.
Constraints:
- Product and category pages that search engines can index (
README.md:11). - Live updates over WebSocket (
README.md:19). - Analytics through a layer that can be switched off or mocked (
README.md:20,AGENTS.md:18). - Only the API through the generated SDK; never hand-written API types (
AGENTS.md:9,AGENTS.md:19). - Design-system components, never forked (
AGENTS.md:11). - Never trust the browser for anything authoritative (
AGENTS.md:17). - Payment entry stays in checkout (
AGENTS.md:13).
Known in code: None. The repo holds docs only.
Track: full — the frameworks need comparing against outside sources · Appetite: 3h active (Default)
| ID | Research question | Track | Why it matters to the plan |
|---|---|---|---|
| RQ-1 | Which rendering approach for indexable catalog and marketing pages: Nuxt 4 hybrid rendering (routeRules, SWR/ISR), Astro with Vue islands, or Vite SSR? | technical | The framework |
| RQ-2 | Which data layer over the generated SDK (useFetch, Pinia Colada or TanStack Query), and how are cookies and the session forwarded during SSR? |
technical | Data fetching and auth for the whole app |
| RQ-3 | Which SEO, performance and accessibility tooling: unhead, sitemap and structured data, Lighthouse CI budgets, axe? | technical | Measurable SEO, performance and accessibility in CI |
| RQ-4 | Security and serving: CSP and headers (nuxt-security), XSS risk in serialized SSR state, and the size of the server image (for example a Nitro node-server)? | technical | Headers and the container |
| RQ-5 | Real-time WebSocket updates, the analytics abstraction, and does an installable PWA earn its place? | technical | Live updates, analytics, and the mobile Default |
Assumptions: Web only (Default). Ionic's UI kit would duplicate the design system.
Spec
Not written yet.
Plan
Not written yet.
Decisions
| ID | Decision | Kind | Source |
|---|---|---|---|
| D1 | Nuxt 4.x on the node-server preset, with future.compatibilityVersion: 5 and nuxt/server so the Nuxt 5 upgrade stays small. Astro and hand-rolled Vite SSR are rejected. |
Decided | @ABilenduke, 2026-10-07; research RQ-1 |
| D2 | Rendering by route: prerender marketing pages that show no prices; server-render catalog, product, search and price-bearing pages with no swr/isr cache; render account, cart, orders and subscriptions in the browser only, with noindex. Cached routes never read or set cookies. |
Decided | @ABilenduke, 2026-10-07; research RQ-1, F12 |
| D3 | Data: useAsyncData with the openapi-fetch SDK for server-rendered reads; Pinia Colada for signed-in state in the browser. |
Decided | @ABilenduke, 2026-10-07; research RQ-2 |
| D4 | Session: option A, a host-only __Host- cookie on the API host, with the storefront server holding no session. Confirmed by commerce#3 DE-9, which separates customer and staff sessions; the backend ADR specifies the attributes. |
Decided | @ABilenduke, 2026-10-07; confirmed 2026-10-08; commerce#3 DE-9 |
| D5 | Security: nuxt-security with a nonce-based strict CSP, the full header set, hidePoweredBy and SRI, and its rate limiter off. No server islands or server components, and no authorization in routeRules. Patch Nuxt quickly. |
Decided | @ABilenduke, 2026-10-07; research RQ-4 |
| D6 | SEO, accessibility and performance: useSeoMeta, @nuxtjs/sitemap, @nuxtjs/robots, nuxt-schema-org, eslint-plugin-vuejs-accessibility, @axe-core/playwright and Lighthouse CI budgets. nuxt-og-image and IPX are deferred. |
Decided | @ABilenduke, 2026-10-07; research RQ-3 |
| D7 | Real-time and analytics: a single browser-only useWebSocket plugin that refetches on reconnect, and a useAnalytics() interface with noop, mock and provider adapters behind consent. No PWA. |
Decided | @ABilenduke, 2026-10-07; research RQ-5 |
| D8 | Container: copy only .output onto distroless nodejs nonroot, per commerce#3 DE-5. |
Decided | @ABilenduke, 2026-10-07; research RQ-4; commerce#3 DE-5 |
ADR: to be written through a PR in this repo.
Links
- Epic: Choose the tech stacks commerce#3. It answers the shared baseline: the JS/TS runtime and package manager, the shared lint/format/type/test toolchain, the CI security checks, the container baseline, the quality gate and the market sample.
- Research: Choose the storefront stack #1 (comment)
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
ABilenduke commentedon Oct 7, 2026
Time: issue-1
Generated by
worklog renderfromtime.jsonl. Do not edit by hand.Active time counts agent turns plus up to 5 minutes of each wait for a person, and no single gap
counts more than 30 minutes.
—means nothing was recorded.workflow · claude-code · 2026-10-07 00:55
ABilenduke commentedon Oct 7, 2026
Research: Choose the storefront stack
Status: Conditional (the session cookie and SDK client depend on the backend spike) · Evidence base:
abd7b64(clean), plusd0573a0for the parent docs · 2026-10-07Summary
Build the storefront on Nuxt 4, rendering each kind of page the way it needs:
Public data comes through the generated SDK with Nuxt's
useAsyncData. Signed-in data uses Pinia Colada, which WebSocket events can refresh.Security comes from
nuxt-security, using a nonce-based strict CSP. By default Nuxt sends no security headers at all, and it had ten security advisories in June and July 2026. So patch quickly, and never use server islands.No PWA: a service worker solves no listed problem and risks showing stale prices.
Confidence is high on the framework and security, and medium on the data layer. The session cookie and the SDK client depend on decisions the backend spike makes.
Answers
routeRulesper route:• prerender the static marketing pages;
• render on the server, with no page cache, every page that shows prices or stock;
• render account and cart pages in the browser only.
Astro 7 with Vue islands has a better cache API (tags), but it splits the app into isolated islands and needs
node_modulesat runtime. Hand-rolled Vite SSR is an API meant for framework authors.useAsyncDatawith the SDK for public reads rendered on the server, and Pinia Colada for signed-in state and changes in the browser, refreshed by key when a WebSocket event arrives. TanStack Query is the fallback. For safety, the session cookie should be a host-only__Host-cookie on the API host (option A) or behind a same-origin/apiproxy (option B), neverDomain=rsl-commerce.test. SameSite does not stop CSRF between sibling subdomains, so the backend must still check Origin and require a custom header or token.useSeoMeta(built in), plus@nuxtjs/sitemap,@nuxtjs/robotsandnuxt-schema-org, installed one by one. Defernuxt-og-imageand the@nuxtjs/seobundle. For accessibility,eslint-plugin-vuejs-accessibility(in the shared config) and@axe-core/playwrighton the key journeys. Use Lighthouse CI with budgets (pinned, because it bundles an outdated Lighthouse). Add@nuxt/imageonce we know where images are hosted.nuxt-security: by default Nuxt sends onlyx-powered-by: Nuxt. Use a nonce-based strict CSP;style-srcneeds'unsafe-inline'. The data Nuxt embeds in the page is escaped and parsed without eval (verified). ButruntimeConfig.publicis emitted as executable JS, so it must hold only static values..outputis self-contained at 2.9 MB; a distroless Node 24 image came to about 218 MB.useWebSocket; events are hints to refetch, and it refetches after every reconnect. Analytics: an in-repouseAnalytics()interface withnoop,mockand provider adapters, behind a consent gate. PWA: no. Installability needs only a manifest, and offline use conflicts with authoritative prices.Recommendation
Framework: Nuxt 4.x, on the Nitro node-server preset.
future.compatibilityVersion: 5early, and write server code againstnuxt/server. The Nuxt 5 upgrade is then small (F8). Nuxt 4 keeps at least six months of support after Nuxt 5 ships (F6).node_modules(F61), and it has had three majors in 19 months (F7). It would be the better choice only if the storefront were mostly content.Rendering by route
prerender: trueswr/israccount/**, cart, orders, subscriptionsssr: false, noindexIf load ever justifies caching, add a short
swrto category pages only, and those pages must never read or set cookies. Nitro hides cookies from cached renders and replays a cachedSet-Cookieto every visitor (F12).Data layer
useAsyncData(key, () => sdk.op(...)). Never call a bare$fetchinsetup; it fetches twice (F23).awaitduring SSR, and refreshes cached results by key (F25).clear()after every server render (F27).openapi-fetchdoes; Hey API's Nuxt client is still beta (F33). The backend spike decides this.Session cookie (the backend spike decides this too)
__Host-cookie onapi.rsl-commerce.testwithSameSite=Lax. CORS allows credentials only fromhttps://rsl-commerce.test. The storefront server never sees a session, so caching is safe by construction (F34).rsl-commerce.test/api/**proxy, through Nitro or the platform's proxy. It allows signed-in pages to render on the server. But every call takes an extra hop, h3's proxy forwards nearly every header and can't proxy a WebSocket upgrade, and the WebSocket then needs a ticket (F32).Domain=rsl-commerce.test. Rejected: the cookie would also be sent to admin, checkout and docs (F29).SEO, performance and accessibility
useSeoMeta.@nuxtjs/sitemap, with adefineSitemapEventHandlerthat calls the SDK.@nuxtjs/robots, with noindex outside production and on search, account and cart pages.nuxt-schema-orgfor Product, Offer and BreadcrumbList data, with prices exactly as the backend returns them.Security
nuxt-security:script-src 'nonce-{{nonce}}' 'strict-dynamic',object-src 'none',base-uri 'none';form-action 'self', plus the checkout origin if the hand-off POSTs;connect-src 'self' https://api.rsl-commerce.test wss://api.rsl-commerce.test;frame-ancestors 'none', COOP and Referrer-Policy;hidePoweredByand SRI on;renderJsonPayloadson; put no secrets inuseState; no server islands or server components; no authorization inrouteRules(F57).Container: copy only
.outputand run as non-root (F60, F62). The base image follows the epic's container baseline. Distroless was the smallest here, and has no shell.Real-time and analytics
plugins/realtime.client.tsholds a single VueUseuseWebSocketconnection that closes on logout. Events map to Pinia Colada refreshes, and every reconnect refetches (F64, F73). The backend checks Origin on the handshake (F66).useAnalytics()adapters are chosen through runtime config. A third-party provider would load through@nuxt/scriptswithuseScriptTriggerConsent(F67). Checkout gets no analytics (F68).PWA: not adopted (F69-F74). Revisit only if push notifications become a requirement.
Risks and unknowns
storefront/.github/dependabot.yml:8).swr/isrpages (F47). If caching is added, verify that a nonce is never served from cache./api/**..output. Image sizes are local, uncompressed values.What this means for the plan
Candidate decisions for the storefront ADR:
useAsyncDataplus Pinia Colada.useAnalytics()behind a consent gate.Candidate requirements:
.outputand runs as non-root.runtimeConfig.publicholds only static, non-secret values.Verified by the lead before posting:
npm view;>= 4.4.0, <= 4.5.0, patched in 4.5.1), through the GitHub advisories API;'unsafe-inline'for styles);nitropack/dist/runtime/internal/cache.mjs:165-176,312-318in the experiment install.Evidence
RQ-1: Rendering approach
storefront/README.md:8-20@abd7b64)storefront/README.md:24,storefront/AGENTS.md:17@abd7b64;AGENTS.md:21-22@d0573a0)storefront/AGENTS.md:11-12@abd7b64;design-system/README.md:3-5,21-22@3e8fd6e)AGENTS.md:23@d0573a0;storefront/README.md:28@abd7b64)AGENTS.md:29-30,README.md:59-61@d0573a0)nuxt4.6.0 (2026-10-05), with no 5.x published. Minors: 4.0 Jul 2025 → 4.6 Oct 2026.nitro3.0.260903-beta.astro7.3.6. Astro majors 5.0 (2024-12), 6.0 (2026-03) and 7.0 (2026-06).@astrojs/vue7.0.3,vite8.3.3,vue3.5.43.nuxt/serverto "smooth the upgrade to Nuxt 5"; the Nuxt 5 defaults can be tried withfuture.compatibilityVersion: 5. (Nuxt releases, accessed 2026-10-07, v4.6.0)routeRulesoffersssr,swr,isr,prerender,headers,noScriptsandappMiddleware. Hybrid rendering is unavailable withnuxt generate. (Nuxt Rendering Modes, accessed 2026-10-07, 4.x)cache:storage is in memory in production and can be set to Redis. Invalidation is manual throughuseStorage('cache').removeItem, with no tag API. (Nitro Cache, accessed 2026-10-07, Nitro v2)varies, and replay a cachedset-cookieto every client. (nitropack/dist/runtime/internal/cache.mjs:165-176,312-318, nitropack 2.13.4) A live test (E4): theswrroute renderedNO-COOKIEforCookie: session=alice, while the plain SSR route renderedsession=aliceandsession=bob.client:*directives. Functions can't be passed to hydrated components. (Astro: Front-end frameworks, accessed 2026-10-07, Astro 7)appEntrypointlets you callapp.usefor Vue plugins. (@astrojs/vue, accessed 2026-10-07, 7.0.3)cache.set({maxAge, swr, tags})with invalidation by tag or path, plus a memory provider for Node. (Astro route caching, accessed 2026-10-07, Astro 7)node_modules(F61) cost more than its cache tags (F17) save, especially since F2 and F4 already rule out caching price pages.RQ-2: Data layer and session
storefront/AGENTS.md:9-10,19@abd7b64)rsl-commerce.testand itsapi.,admin.,checkout.anddocs.subdomains. (backend/README.md:12,22@38a8548;README.md:20-26@d0573a0)$fetchin setup fetches twice;useFetch/useAsyncDatapass the data through the payload. For external APIs,useRequestHeadersmust be used "very carefully", and cookies set during SSR must be passed back manually. (Nuxt Data Fetching, accessed 2026-10-07, 4.x)$fetch.createpluscreateUseFetch(). (Nuxt custom useFetch, accessed 2026-10-07, 4.x)@pinia/colada-nuxtrequires@pinia/nuxtand "doesn't requireawaitfor SSR" (it usesonServerPrefetch). (Pinia Colada Nuxt, accessed 2026-10-07, 1.x)@pinia/colada1.4.7 (1.0 released 2026-03-06) has one npm maintainer;@tanstack/vue-query5.104.1 has three. (E1)useState.gcTimeisInfinityon the server, so callqueryClient.clear(). (TanStack Query Vue SSR, accessed 2026-10-07, v5)Domaina cookie is host-only; with it, the cookie covers all subdomains.__Host-requires Secure, no Domain andPath=/. (MDN Set-Cookie, accessed 2026-10-07)Sec-Fetch-Sitechecks and__Host-cookies. (OWASP CSRF Cheat Sheet, accessed 2026-10-07)routeRulesproxyoption can target an external URL. (Nitro config, accessed 2026-10-07, v2)Set-Cookieback with an optionalcookieDomainRewrite, and does not proxy WebSocket upgrades. (h3/dist/index.mjs:1138-1147,1205-1225,1253-1261;nitropack runtime/internal/route-rules.mjs:43-61) (E6)RQ-3: SEO, performance and accessibility tooling
useHead,useSeoMetaanduseServerSeoMeta. Never pass user input toinnerHTML. (Nuxt SEO and Meta, accessed 2026-10-07, 4.x)@nuxtjs/seobundles robots, sitemap, og-image, schema-org, link-checker, seo-utils and site-config; "every module works standalone". (Nuxt SEO, accessed 2026-10-07, 5.x)sourcesordefineSitemapEventHandler, cached for 10 minutes by default. (Sitemap dynamic URLs, accessed 2026-10-07, 8.x)@nuxtjs/seo5.3.16 andnuxt-og-image6.10.3 have one maintainer.@lhci/cli0.15.1 (2025-06-25) bundles Lighthouse 12.6.1, whilelighthouseis at 13.5.0.eslint-plugin-vuejs-accessibilityis at 2.6.0 (2026-08-10).@nuxt/imageis at 2.1.0.AxeBuilder().withTags(['wcag2a','wcag2aa']); "many accessibility problems can only be discovered through manual testing". (Playwright accessibility testing, accessed 2026-10-07).vuefiles. (repo, accessed 2026-10-07, 2.x)design-system/README.md:12,16,design-system/AGENTS.md:14@3e8fd6e)RQ-4: Security and serving
'strict-dynamic'. "Allowing'unsafe-inline'for styles is widely considered as acceptable"; setimg-srcproperly. (Strict CSP, accessed 2026-10-07, 2.x)script-srcincludes the nonce and'strict-dynamic'. "Nonce only works for SSR"; the page says nothing about cachedswr/isrpages. (CSP, accessed 2026-10-07, 2.x)nuxt-security2.6.0 (2026-05-12) has two maintainers, and has released regularly since 2025. (E1)content-typeandx-powered-by: Nuxt; there is no CSP and no HSTS. (E3)<script type="application/json" id="__NUXT_DATA__">and in_payload.json; zero raw</script><script>alertoccurrences. The renderer uses devaluestringifyplus slash encoding, butunevals the config into an inline executable script. (nuxt/dist/runtime/server/renderer/payload.js:17-29,36-38;nuxt/dist/app/composables/payload.js:184-201) (E3, E6)</script>.unevaloutput is code, so onlyparseshould handle untrusted input. (devalue, accessed 2026-10-07, 6.x)renderJsonPayloadsis on by default. (Nuxt experimental features, accessed 2026-10-07, 4.x)<NoScript>, and XSS through<NuxtLink>javascript:URLs. (Nuxt security advisories, accessed 2026-10-07)node .output/server/index.mjs, configured throughNITRO_PORT/PORT. (Nuxt Deployment, accessed 2026-10-07, 4.x).outputis "fully standalone". (Nitro Node runtime, accessed 2026-10-07, v2)node_modules. (Astro Docker recipe, accessed 2026-10-07, Astro 7).output2.9 MB (server 2.6 MB, including 2.2 MB of tracednode_modules). Images that copy only.output: distrolessnodejs24-debian13:nonroot217.9 MB,node:24-alpine237.7 MB,node:24-slim333.4 MB; the app layer is 3 MB. The distroless container served 200 and has no/bin/sh. (E2, E5)RQ-5: Real-time, analytics, PWA
storefront/README.md:19-20,storefront/AGENTS.md:18@abd7b64). Browser events are browser-safe (backend/AGENTS.md:13-14@38a8548).useWebSockethasautoReconnect,heartbeatand buffering, and runs in the browser only. (VueUse useWebSocket, accessed 2026-10-07, @vueuse/core 15)useScriptTriggerConsent, the script loads only after consent. (Nuxt Scripts consent, accessed 2026-10-07, v1)checkout/README.md:24@98fcf1e)@vite-pwa/nuxt1.1.1 (2026-02-06) depends onvite-plugin-pwa ^1.2.0, while 2.0.0 is current. (E1)AGENTS.md:27-28@d0573a0).Current state
The repo is docs-only:
storefront/README.mdandstorefront/AGENTS.md;actions(storefront/.github/workflows/codeql.yml:24);storefront/.github/dependabot.yml:8).Sources
Docs and pages opened on 2026-10-07:
Experiments, all run in a scratch directory with no repository changes:
npm view <pkg> version time dependencies maintainers --jsonfor each candidate.npx nuxt buildprintedΣ Total size: 2.12 MB (537 kB gzip).du -sh:.output2.9M, server 2.6M, itsnode_modules2.2M, public 256K.PORT=3917 node .output/server/index.mjs, thencurl -D - /p/demo.cache-control: s-maxage=60, stale-while-revalidate,x-powered-by: Nuxt.grep -c "</script><script>alert"returned0.curl -H 'Cookie: session=alice' /p/meon theswrroute returnedNO-COOKIE./account/mereturnedsession=alice, andsession=bobfor bob.docker buildwithCOPY .outputontonode:24-slim,gcr.io/distroless/nodejs24-debian13:nonrootandnode:24-alpine.GET /p/demo -> 200.docker exec … /bin/shfailed withno such file or directory.workflow · claude-code · 2026-10-07 00:26