Skip to content

Choose the storefront stack #1

Closed

Description

@ABilenduke

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

Activity

  1. added
    type:spikeResearch that answers a question and ends there
    track:fullNeeds evidence: brief, research, plan, execute, review
    on Oct 7, 2026
  2. ABilenduke commented on Oct 7, 2026

    @ABilenduke
    ContributorAuthor

    Time: issue-1

    Generated by worklog render from time.jsonl. Do not edit by hand.

    Step Estimate Started Finished Wall Active Sessions
    brief 15m 2026-10-07 00:03 2026-10-07 00:04 0m — 3da8d26d (claude-code)
    research 45m 2026-10-07 00:24 2026-10-07 00:26 2m 2m 3da8d26d (claude-code)
    Total 1h 2m 2m

    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

  3. ABilenduke commented on Oct 7, 2026

    @ABilenduke
    ContributorAuthor

    Research: Choose the storefront stack

    Status: Conditional (the session cookie and SDK client depend on the backend spike) · Evidence base: abd7b64 (clean), plus d0573a0 for the parent docs · 2026-10-07

    Summary

    Build the storefront on Nuxt 4, rendering each kind of page the way it needs:

    • prerender marketing pages;
    • render product, category and search pages on the server, with no page cache, so prices and stock are always the backend's;
    • render account and cart pages only in the browser.

    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

    RQ Answer Confidence
    RQ-1 Nuxt 4 (4.6.0 is current; Nuxt 5 with Nitro 3 is expected in Q4 2026, and Nitro 3 is still in beta), with routeRules per 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_modules at runtime. Hand-rolled Vite SSR is an API meant for framework authors.
    High for the framework, medium for the route modes
    RQ-2 Use useAsyncData with 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 /api proxy (option B), never Domain=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. Medium
    RQ-3 Use Unhead's useSeoMeta (built in), plus @nuxtjs/sitemap, @nuxtjs/robots and nuxt-schema-org, installed one by one. Defer nuxt-og-image and the @nuxtjs/seo bundle. For accessibility, eslint-plugin-vuejs-accessibility (in the shared config) and @axe-core/playwright on the key journeys. Use Lighthouse CI with budgets (pinned, because it bundles an outdated Lighthouse). Add @nuxt/image once we know where images are hosted. Medium
    RQ-4 Adopt nuxt-security: by default Nuxt sends only x-powered-by: Nuxt. Use a nonce-based strict CSP; style-src needs 'unsafe-inline'. The data Nuxt embeds in the page is escaped and parsed without eval (verified). But runtimeConfig.public is emitted as executable JS, so it must hold only static values. .output is self-contained at 2.9 MB; a distroless Node 24 image came to about 218 MB. High
    RQ-5 Real-time: one plugin that runs only in the browser and wraps VueUse useWebSocket; events are hints to refetch, and it refetches after every reconnect. Analytics: an in-repo useAnalytics() interface with noop, mock and provider adapters, behind a consent gate. PWA: no. Installability needs only a manifest, and offline use conflicts with authoritative prices. Medium for real-time and analytics, high for the PWA verdict

    Recommendation

    Framework: Nuxt 4.x, on the Nitro node-server preset.

    • Turn on future.compatibilityVersion: 5 early, and write server code against nuxt/server. The Nuxt 5 upgrade is then small (F8). Nuxt 4 keeps at least six months of support after Nuxt 5 ships (F6).
    • Alternatives:
      • Astro 7 with Vue islands. It has cache tags (F17) and ships no JS on marketing pages. But it cuts the Vue app context at every island (F15), its SSR image needs 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.
      • Hand-rolled Vite SSR. Rejected (F19): we would rebuild payload escaping, head management, routing and caching ourselves.

    Rendering by route

    Routes Mode Why
    Marketing and landing pages with no prices prerender: true Static content
    Home, campaign, promotion, category, product and search pages SSR, no swr/isr Prices are authoritative. Nitro's cache is in-process with no tag invalidation, and the storefront may not use Redis (F4, F11). The backend already caches (F22).
    account/**, cart, orders, subscriptions ssr: false, noindex Signed-in data, no SEO value

    If load ever justifies caching, add a short swr to category pages only, and those pages must never read or set cookies. Nitro hides cookies from cached renders and replays a cached Set-Cookie to every visitor (F12).

    Data layer

    • Public reads rendered on the server use useAsyncData(key, () => sdk.op(...)). Never call a bare $fetch in setup; it fetches twice (F23).
    • Signed-in state in the browser uses Pinia Colada: it has an official Nuxt module, needs no await during SSR, and refreshes cached results by key (F25).
    • TanStack Query also works, with a larger maintainer pool. Its Nuxt setup is a hand-written plugin, and it needs clear() after every server render (F27).
    • The SDK client must accept a custom fetch or middleware. openapi-fetch does; Hey API's Nuxt client is still beta (F33). The backend spike decides this.

    Session cookie (the backend spike decides this too)

    • A (recommended): a host-only __Host- cookie on api.rsl-commerce.test with SameSite=Lax. CORS allows credentials only from https://rsl-commerce.test. The storefront server never sees a session, so caching is safe by construction (F34).
    • B: a same-origin 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).
    • C: Domain=rsl-commerce.test. Rejected: the cookie would also be sent to admin, checkout and docs (F29).
    • D: a storefront-owned session (BFF). Rejected: the backend owns sessions (F22).

    SEO, performance and accessibility

    • useSeoMeta.
    • @nuxtjs/sitemap, with a defineSitemapEventHandler that calls the SDK.
    • @nuxtjs/robots, with noindex outside production and on search, account and cart pages.
    • nuxt-schema-org for Product, Offer and BreadcrumbList data, with prices exactly as the backend returns them.
    • axe with the WCAG 2 A/AA tags on home, category, product, search, cart and account, plus manual checks (F41).
    • Lighthouse CI budgets against the production container (F40).

    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;
      • HSTS, nosniff, frame-ancestors 'none', COOP and Referrer-Policy;
      • hidePoweredBy and SRI on;
      • its rate limiter off, because the limiter is per-process (F48).
    • Rules: keep renderJsonPayloads on; put no secrets in useState; no server islands or server components; no authorization in routeRules (F57).

    Container: copy only .output and 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.ts holds a single VueUse useWebSocket connection 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/scripts with useScriptTriggerConsent (F67). Checkout gets no analytics (F68).

    PWA: not adopted (F69-F74). Revisit only if push notifications become a requirement.

    Risks and unknowns

    • Nuxt 5 is close. It is expected in Q4 2026 and Nitro 3 is still in beta (F6, F9), so plan a migration within months of starting.
    • Nuxt patch cadence. Ten advisories came out in June and July 2026, including high-severity server-island RCE, route-rule bypasses and a payload-cache leak (F56, F57). Add an npm ecosystem to Dependabot when code lands (storefront/.github/dependabot.yml:8).
    • Nonces on cached pages. nuxt-security doesn't document nonces on swr/isr pages (F47). If caching is added, verify that a nonce is never served from cache.
    • Bus factor. Pinia Colada and all the Nuxt SEO modules each have one npm maintainer (F26, F39). Lighthouse CI hasn't released since June 2025 (F40).
    • Open across repos:
      • the SDK generator and its fetch adapter;
      • the cookie attributes, the CORS allowlist and the CSRF scheme;
      • the WebSocket protocol;
      • where analytics events go;
      • where images are hosted;
      • whether the platform proxy routes /api/**.
    • Experiment limits. The test app was minimal, so the design system and modules will grow .output. Image sizes are local, uncompressed values.

    What this means for the plan

    Candidate decisions for the storefront ADR:

    • D1: Nuxt 4.x on node-server, with Nuxt 5 planned for.
    • D2: rendering by route as in the table above.
    • D3: session cookie option A or B, together with the backend ADR.
    • D4: useAsyncData plus Pinia Colada.
    • D5: an SDK client with custom fetch or middleware support.
    • D6: nuxt-security with the headers above; no server islands.
    • D7: a real-time plugin that runs only in the browser.
    • D8: useAnalytics() behind a consent gate.
    • D9: no PWA.

    Candidate requirements:

    • R1: Cached routes never read or set cookies.
    • R2: Only the backend decides authorization.
    • R3: Lint and axe gates on the key journeys, plus Lighthouse budgets.
    • R4: The image copies only .output and runs as non-root.
    • R5: runtimeConfig.public holds only static, non-secret values.
    • R6: Analytics adapters can be switched by config.

    Verified by the lead before posting:

    • the versions, through npm view;
    • the Nuxt advisories and GHSA-wm8w-6qjm-cv43 (CVE-2026-71316, high, >= 4.4.0, <= 4.5.0, patched in 4.5.1), through the GitHub advisories API;
    • the Nuxt roadmap (Nuxt 5 in Q4 2026, a six-month overlap, Nuxt 3 end of life on 31 July 2026);
    • the nuxt-security strict-CSP page (nonces for SSR, hashes for SSG, 'unsafe-inline' for styles);
    • nitropack/dist/runtime/internal/cache.mjs:165-176,312-318 in the experiment install.
    Evidence

    RQ-1: Rendering approach

    • F1 [verified-code] Scope: marketing pages; indexable product and category pages; search; accounts; the cart and checkout hand-off; order history; subscriptions; live updates; and analytics that can be switched off. (storefront/README.md:8-20 @ abd7b64)
    • F2 [verified-code] Prices and stock are authoritative on the backend. (storefront/README.md:24, storefront/AGENTS.md:17 @ abd7b64; AGENTS.md:21-22 @ d0573a0)
    • F3 [verified-code] Components and tokens come from the design system, which does no data fetching. (storefront/AGENTS.md:11-12 @ abd7b64; design-system/README.md:3-5,21-22 @ 3e8fd6e)
    • F4 [verified-code] Frontends never talk to PostgreSQL, Redis or RabbitMQ. (AGENTS.md:23 @ d0573a0; storefront/README.md:28 @ abd7b64)
    • F5 [verified-code] Every technology must solve a real problem. (AGENTS.md:29-30, README.md:59-61 @ d0573a0)
    • F6 [sourced] Nuxt 5 "will include an upgrade to Nitro v3" and is planned for Q4 2026 (estimated). Each major is supported for at least six months after the next major ships. Nuxt 3 reached end of life on 31 July 2026. (Nuxt Roadmap, accessed 2026-10-07, Nuxt 4.x)
    • F7 [experiment] npm registry (E1):
      • nuxt 4.6.0 (2026-10-05), with no 5.x published. Minors: 4.0 Jul 2025 → 4.6 Oct 2026.
      • nitro 3.0.260903-beta.
      • astro 7.3.6. Astro majors 5.0 (2024-12), 6.0 (2026-03) and 7.0 (2026-06).
      • @astrojs/vue 7.0.3, vite 8.3.3, vue 3.5.43.
    • F8 [sourced] v4.6.0 adds nuxt/server to "smooth the upgrade to Nuxt 5"; the Nuxt 5 defaults can be tried with future.compatibilityVersion: 5. (Nuxt releases, accessed 2026-10-07, v4.6.0)
    • F9 [sourced] The latest Nitro is v3.0.260903-beta, still in beta; v2.13.4 is the latest v2. (Nitro releases, accessed 2026-10-07)
    • F10 [sourced] routeRules offers ssr, swr, isr, prerender, headers, noScripts and appMiddleware. Hybrid rendering is unavailable with nuxt generate. (Nuxt Rendering Modes, accessed 2026-10-07, 4.x)
    • F11 [sourced] Nitro's cache: storage is in memory in production and can be set to Redis. Invalidation is manual through useStorage('cache').removeItem, with no tag API. (Nitro Cache, accessed 2026-10-07, Nitro v2)
    • F12 [experiment] Cached Nitro handlers pass the renderer only the headers listed in varies, and replay a cached set-cookie to every client. (nitropack/dist/runtime/internal/cache.mjs:165-176,312-318, nitropack 2.13.4) A live test (E4): the swr route rendered NO-COOKIE for Cookie: session=alice, while the plain SSR route rendered session=alice and session=bob.
    • F13 [sourced] Astro 7.0 (2026-06-22) brings Vite 8, a Rust compiler and a stable route-caching API. (Astro 7.0, accessed 2026-10-07)
    • F14 [sourced] Framework components render to static HTML by default and hydrate through client:* directives. Functions can't be passed to hydrated components. (Astro: Front-end frameworks, accessed 2026-10-07, Astro 7)
    • F15 [sourced] Context wrappers can't be shared across islands; the recipe uses Nano Stores. (Astro: Share state between islands, accessed 2026-10-07, Astro 7)
    • F16 [sourced] appEntrypoint lets you call app.use for Vue plugins. (@astrojs/vue, accessed 2026-10-07, 7.0.3)
    • F17 [sourced] 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)
    • F18 [sourced] Cloudflare acquired Astro on 2026-01-16; it stays MIT-licensed and platform-agnostic. (Astro joins Cloudflare, accessed 2026-10-07)
    • F19 [sourced] Vite's SSR guide calls it "a low-level API meant for library and framework authors". (Vite SSR, accessed 2026-10-07, Vite 8)
    • F20 [inference] From F1-F19: the storefront is half content and half app around one design system and shared cart state. Nuxt gives one Vue app with a mode per route. Astro's isolated islands (F15) and runtime 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

    • F21 [verified-code] The storefront uses the API only through the generated SDK and never hand-writes API types. (storefront/AGENTS.md:9-10,19 @ abd7b64)
    • F22 [verified-code] The backend owns auth and sessions and caches in Redis. The hosts are rsl-commerce.test and its api., admin., checkout. and docs. subdomains. (backend/README.md:12,22 @ 38a8548; README.md:20-26 @ d0573a0)
    • F23 [sourced] A bare $fetch in setup fetches twice; useFetch/useAsyncData pass the data through the payload. For external APIs, useRequestHeaders must be used "very carefully", and cookies set during SSR must be passed back manually. (Nuxt Data Fetching, accessed 2026-10-07, 4.x)
    • F24 [sourced] A custom fetch is made with $fetch.create plus createUseFetch(). (Nuxt custom useFetch, accessed 2026-10-07, 4.x)
    • F25 [sourced] @pinia/colada-nuxt requires @pinia/nuxt and "doesn't require await for SSR" (it uses onServerPrefetch). (Pinia Colada Nuxt, accessed 2026-10-07, 1.x)
    • F26 [experiment] @pinia/colada 1.4.7 (1.0 released 2026-03-06) has one npm maintainer; @tanstack/vue-query 5.104.1 has three. (E1)
    • F27 [sourced] TanStack Query's Nuxt plugin is hand-written with useState. gcTime is Infinity on the server, so call queryClient.clear(). (TanStack Query Vue SSR, accessed 2026-10-07, v5)
    • F28 [sourced] Subdomains are same-site but cross-origin. (web.dev: same-site and same-origin, accessed 2026-10-07)
    • F29 [sourced] Without Domain a cookie is host-only; with it, the cookie covers all subdomains. __Host- requires Secure, no Domain and Path=/. (MDN Set-Cookie, accessed 2026-10-07)
    • F30 [sourced] SameSite "does not replace a proper CSRF defense", and sibling subdomains can bypass it. The recommendations are custom headers, Sec-Fetch-Site checks and __Host- cookies. (OWASP CSRF Cheat Sheet, accessed 2026-10-07)
    • F31 [sourced] The Nitro routeRules proxy option can target an external URL. (Nitro config, accessed 2026-10-07, v2)
    • F32 [experiment] h3 1.15.11's proxy forwards every request header except a short hop-by-hop list, passes Set-Cookie back with an optional cookieDomainRewrite, 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)
    • F33 [sourced] Hey API's Nuxt client is labelled Beta (@hey-api/openapi-ts 0.99). openapi-fetch is about 6 kB, typed from openapi-typescript, with middleware. (Hey API Nuxt client; openapi-fetch, accessed 2026-10-07)
    • F34 [inference] From F12, F22, F28-F32: SameSite doesn't separate the subdomains, so a host-only cookie limits who receives it. Option A also keeps the storefront server free of sessions, which makes caching safe by construction.

    RQ-3: SEO, performance and accessibility tooling

    • F35 [sourced] Unhead provides useHead, useSeoMeta and useServerSeoMeta. Never pass user input to innerHTML. (Nuxt SEO and Meta, accessed 2026-10-07, 4.x)
    • F36 [sourced] @nuxtjs/seo bundles 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)
    • F37 [sourced] Sitemap URLs come from sources or defineSitemapEventHandler, cached for 10 minutes by default. (Sitemap dynamic URLs, accessed 2026-10-07, 8.x)
    • F38 [sourced] OG Image renders at build time or on demand, through Takumi, Satori or Chromium. (Nuxt OG Image, accessed 2026-10-07, v6)
    • F39 [experiment] (E1)
      • @nuxtjs/seo 5.3.16 and nuxt-og-image 6.10.3 have one maintainer.
      • nuxt-og-image lists sharp, resvg, takumi and playwright-core as peer dependencies.
      • @lhci/cli 0.15.1 (2025-06-25) bundles Lighthouse 12.6.1, while lighthouse is at 13.5.0.
      • eslint-plugin-vuejs-accessibility is at 2.6.0 (2026-08-10).
      • @nuxt/image is at 2.1.0.
    • F40 [sourced] Lighthouse CI has assertions, budgets and a server. Its latest release is v0.15.1; v0.15.0 upgraded to Lighthouse 12.6.1. (Lighthouse CI releases, accessed 2026-10-07)
    • F41 [sourced] AxeBuilder().withTags(['wcag2a','wcag2aa']); "many accessibility problems can only be discovered through manual testing". (Playwright accessibility testing, accessed 2026-10-07)
    • F42 [sourced] eslint-plugin-vuejs-accessibility checks accessibility rules in .vue files. (repo, accessed 2026-10-07, 2.x)
    • F43 [sourced] IPX is the self-hosted optimiser, based on sharp. (Nuxt Image IPX, accessed 2026-10-07, 2.x)
    • F44 [verified-code] The design system owns the accessibility standards and the shared lint config. (design-system/README.md:12,16, design-system/AGENTS.md:14 @ 3e8fd6e)

    RQ-4: Security and serving

    • F45 [sourced] nuxt-security provides security headers, a rate limiter, a request size limiter, an XSS validator, CORS, hiding X-Powered-By, SRI and CSRF. (quick start, accessed 2026-10-07, 2.x)
    • F46 [sourced] It uses nonces for SSR and hashes for SSG, with 'strict-dynamic'. "Allowing 'unsafe-inline' for styles is widely considered as acceptable"; set img-src properly. (Strict CSP, accessed 2026-10-07, 2.x)
    • F47 [sourced] The default script-src includes the nonce and 'strict-dynamic'. "Nonce only works for SSR"; the page says nothing about cached swr/isr pages. (CSP, accessed 2026-10-07, 2.x)
    • F48 [sourced] The rate limiter keeps its counts in an LRU cache in the process: "not suitable for complex production applications". (Rate limiter, accessed 2026-10-07, 2.x)
    • F49 [sourced] The CSRF module is built on nuxt-csurf and is off by default. (CSRF, accessed 2026-10-07, 2.x)
    • F50 [experiment] nuxt-security 2.6.0 (2026-05-12) has two maintainers, and has released regularly since 2025. (E1)
    • F51 [experiment] The default Nuxt 4.6.0 response headers are only content-type and x-powered-by: Nuxt; there is no CSP and no HSTS. (E3)
    • F52 [experiment] An attacker string in the payload was escaped inside <script type="application/json" id="__NUXT_DATA__"> and in _payload.json; zero raw </script><script>alert occurrences. The renderer uses devalue stringify plus slash encoding, but unevals 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)
    • F53 [sourced] devalue escapes </script>. uneval output is code, so only parse should handle untrusted input. (devalue, accessed 2026-10-07, 6.x)
    • F54 [sourced] devalue published ten advisories between March and September 2026, two of them high severity. (devalue advisories, accessed 2026-10-07)
    • F55 [sourced] renderJsonPayloads is on by default. (Nuxt experimental features, accessed 2026-10-07, 4.x)
    • F56 [sourced] CVE-2026-71316 (high): Nuxt 4.4.0–4.5.0 cached payloads by path alone, disclosing one user's SSR data to others. It was patched in 4.5.1, published 2026-07-27. (GHSA-wm8w-6qjm-cv43, accessed 2026-10-07)
    • F57 [sourced] On 2026-07-27: an RCE through server island props (high), route rules dropped for mixed-case paths "bypassing appMiddleware auth gates" (high), island DoS issues (high) and a DevTools RPC RCE (critical, dev only). On 2026-06-02: a route-rule middleware bypass (high), XSS through <NoScript>, and XSS through <NuxtLink> javascript: URLs. (Nuxt security advisories, accessed 2026-10-07)
    • F58 [sourced] After the 4.5.1 patch, purge CDN caches and declare the props server islands accept. (Nuxt v4.5 security patch, accessed 2026-10-07)
    • F59 [sourced] Run with node .output/server/index.mjs, configured through NITRO_PORT/PORT. (Nuxt Deployment, accessed 2026-10-07, 4.x)
    • F60 [sourced] .output is "fully standalone". (Nitro Node runtime, accessed 2026-10-07, v2)
    • F61 [sourced] Astro's SSR runtime image copies the production node_modules. (Astro Docker recipe, accessed 2026-10-07, Astro 7)
    • F62 [experiment] Build sizes: .output 2.9 MB (server 2.6 MB, including 2.2 MB of traced node_modules). Images that copy only .output: distroless nodejs24-debian13:nonroot 217.9 MB, node:24-alpine 237.7 MB, node:24-slim 333.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

    • F63 [verified-code] Requirements: live updates and an analytics layer that can be switched off or mocked (storefront/README.md:19-20, storefront/AGENTS.md:18 @ abd7b64). Browser events are browser-safe (backend/AGENTS.md:13-14 @ 38a8548).
    • F64 [sourced] useWebSocket has autoReconnect, heartbeat and buffering, and runs in the browser only. (VueUse useWebSocket, accessed 2026-10-07, @vueuse/core 15)
    • F65 [sourced] The WebSocket API has no backpressure. (MDN WebSocket, accessed 2026-10-07)
    • F66 [sourced] Browsers send cookies on the handshake, so validate Origin against an allowlist and close sockets on logout. (OWASP WebSocket Cheat Sheet, accessed 2026-10-07)
    • F67 [sourced] With useScriptTriggerConsent, the script loads only after consent. (Nuxt Scripts consent, accessed 2026-10-07, v1)
    • F68 [verified-code] Checkout excludes non-essential analytics. (checkout/README.md:24 @ 98fcf1e)
    • F69 [sourced] Installability needs a manifest; a service worker is "not a requirement". (MDN: Making PWAs installable, accessed 2026-10-07)
    • F70 [sourced] On iOS, push works only for Home Screen web apps. (WebKit: Web Push for Web Apps on iOS, accessed 2026-10-07, iOS 16.4)
    • F71 [sourced] @vite-pwa/nuxt describes itself as a "Zero-config PWA Plugin for Nuxt 3". (repo, accessed 2026-10-07)
    • F72 [experiment] @vite-pwa/nuxt 1.1.1 (2026-02-06) depends on vite-plugin-pwa ^1.2.0, while 2.0.0 is current. (E1)
    • F73 [inference] From F63-F66: events are hints. Refetch from the API after each reconnect, and dedupe by key, because duplicates are normal (AGENTS.md:27-28 @ d0573a0).
    • F74 [inference] From F2, F5, F69-F72: a manifest gives installability. Offline use conflicts with authoritative prices, and push is not a requirement. A service worker would solve no listed problem.

    Current state

    The repo is docs-only:

    • storefront/README.md and storefront/AGENTS.md;
    • CodeQL on actions (storefront/.github/workflows/codeql.yml:24);
    • Dependabot for actions only (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:

    • E1, npm registry: npm view <pkg> version time dependencies maintainers --json for each candidate.
      • Key output: nuxt 4.6.0, with zero 5.x versions; nitro 3.0.260903-beta; astro 7.3.6.
      • @lhci/cli 0.15.1 bundles lighthouse 12.6.1, while lighthouse is at 13.5.0.
    • E2, build:
      • The install pulled in nuxt 4.6.0, nitropack 2.13.4, h3 1.15.11, devalue 6.0.2, vite 8.3.3 and vue 3.5.43.
      • npx nuxt build printed Σ Total size: 2.12 MB (537 kB gzip).
      • du -sh: .output 2.9M, server 2.6M, its node_modules 2.2M, public 256K.
    • E3, headers and escaping: PORT=3917 node .output/server/index.mjs, then curl -D - /p/demo.
      • Headers: cache-control: s-maxage=60, stale-while-revalidate, x-powered-by: Nuxt.
      • grep -c "</script><script>alert" returned 0.
    • E4, cookie visibility: curl -H 'Cookie: session=alice' /p/me on the swr route returned NO-COOKIE. /account/me returned session=alice, and session=bob for bob.
    • E5, images: docker build with COPY .output onto node:24-slim, gcr.io/distroless/nodejs24-debian13:nonroot and node:24-alpine.
      • Sizes: 333.4, 217.9 and 237.7 MB.
      • The distroless container returned GET /p/demo -> 200. docker exec … /bin/sh failed with no such file or directory.
      • The containers and images were removed afterwards.
    • E6, source reads: the installed nitropack, h3 and nuxt files at the lines cited in F12, F32 and F52.

    workflow · claude-code · 2026-10-07 00:26

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    stage:doneDonetrack:fullNeeds evidence: brief, research, plan, execute, reviewtype:spikeResearch that answers a question and ends there

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions