Repository navigation
Choose the admin stack #1
Description
Choose the admin 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: 2h active (Default) · Estimate: — · Active so far: see the time comment
Brief
Idea: "Determine the best tech stack for admin. I prefer TypeScript and Vue in some form. Also linters, code quality and security checks. Each service will be in a Docker container."
Problem and audience: Admin is the restricted application staff use to run the store. It has:
- data-heavy screens: catalog, orders, refunds, subscriptions, roles and audit;
- live alerts;
- a UI that mirrors backend permissions without enforcing them;
- no need for SEO.
Outcome: A recorded choice of framework, data, form and table libraries, the session and permission approach, test tools and the serving container. Every requirement below is met, or its gap is named.
Scope.
- In: a Vue + Vite SPA, or Nuxt with
ssr: false. - Out: Ionic, Quasar or Vuetify UI kits, because the design system supplies components. A native app (web only, Default).
- Deferred: an ADR. The shared toolchain and CI security checks come from the epic, commerce#3.
Constraints:
- Hiding a button is not authorization; the backend enforces every permission (
AGENTS.md:8,README.md:20). - Only the API through the generated SDK, plus real-time events (
AGENTS.md:10). - Sensitive actions go through audited backend operations (
AGENTS.md:15). - Each role sees only what it needs (
AGENTS.md:17). - Staff roles (
README.md:12). - Live alerts (
README.md:15). - Components come from the design system (
README.md:21).
Known in code: None. The repo holds docs only.
Track: full — the libraries need comparing against outside sources · Appetite: 2h active (Default)
| ID | Research question | Track | Why it matters to the plan |
|---|---|---|---|
| RQ-1 | Vue + Vite SPA or Nuxt with ssr: false, given that the design system supplies components? |
technical | The framework |
| RQ-2 | Which libraries for the data-heavy UI: tables (TanStack Table), forms and validation (VeeValidate or FormKit with Zod or Valibot), charts, typed routing, and Pinia with a query layer? | technical | The core libraries |
| RQ-3 | How to handle the session and permissions: a cookie session with CSRF protection, UI that mirrors backend RBAC, and no tokens in localStorage? | technical | The security model in the browser |
| RQ-4 | How to handle real-time alerts and the audit views, and which test tools (Vitest browser mode or Playwright component tests)? | technical | Live alerts and the test pyramid |
| RQ-5 | How to serve it as a static container, with security headers and a CSP? | technical | The container |
Assumptions: Web only (Default).
Spec
Not written yet.
Plan
Not written yet.
Decisions
| ID | Decision | Kind | Source |
|---|---|---|---|
| D1 | A Vue 3.5 + Vite 8 SPA with Vue Router 5 typed file-based routing. No Nuxt. | Decided | @ABilenduke, 2026-10-07; research RQ-1 |
| D2 | Libraries: TanStack Table 9 + Virtual behind an admin-owned DataTable; TanStack Form with Zod 4; ECharts 6 via vue-echarts, tree-shaken, with aria on; Pinia 4 + Pinia Colada, holding no persisted server data. Zod schemas are generated by Orval in zod-only mode from the backend's spec (commerce#3 DE-8, decided 2026-10-08). |
Decided | @ABilenduke, 2026-10-07; research RQ-2 |
| D3 | Session and permissions: a cookie-only session, with nothing in Web Storage. A flat permission list from /me, route guards and v-if; no CASL. Server-enforced timeouts and step-up re-auth. The staff session uses its own __Host- cookie with SameSite=Strict, behind an admin-only CORS allowlist and Origin checks (commerce#3 DE-9, decided 2026-10-08). |
Decided | @ABilenduke, 2026-10-07; research RQ-3 |
| D4 | Real-time and tests: VueUse useWebSocket with backoff, jitter, a heartbeat and a refetch on reconnect. Vitest 5 (Node + Browser Mode), MSW 3 and a thin Playwright E2E suite. No Playwright component testing. |
Decided | @ABilenduke, 2026-10-07; research RQ-4 |
| D5 | Container: nginx-unprivileged alpine-slim, read-only, with a header CSP and no unsafe-inline, immutable caching for /assets/ and no-cache for index.html. |
Decided | @ABilenduke, 2026-10-07; research RQ-5; 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 admin 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 admin stack
Status: Answered (the session cookie, CSRF and WebSocket details depend on the backend spike) · Evidence base:
a89f48b(clean), plusd0573a0for the parent docs · 2026-10-07Summary
Build the admin as a plain Vue 3.5 + Vite 8 single-page app with Vue Router 5. Vue Router 5 now includes typed file-based routing.
Nuxt with
ssr: falseadds 66 direct dependencies, telemetry, auto-imports and inline bootstrap scripts that complicate a strict CSP. It buys nothing for an app with no SEO or server rendering.Use headless libraries, so the design system's components render everything:
Mirror backend permissions with a flat permission list from
/me, route guards andv-if; CASL isn't needed. The session lives only in an HttpOnly cookie. Live alerts use VueUseuseWebSocketand refetch after every reconnect.Test with Vitest 5, in Node for logic and in Browser Mode for components, plus MSW and a thin Playwright suite. Serve the app from nginx-unprivileged with a strict header CSP.
Confidence is high for the framework, server and session principles, and medium for the library picks: several are brand-new majors.
Answers
ssr: falseadds 66 direct dependencies (Nitro, devtools, telemetry), auto-imports and inlinewindow.__NUXT__scripts, for no gain without SSR or SEO.Forms: TanStack Form with Standard Schema and Zod 4, which the SDK codegen can emit. VeeValidate's Zod adapter is still on Zod 3, and its v5 beta is stale.
Charts: ECharts 6 via vue-echarts, tree-shaken, with
ariaon.State: Pinia 4 + Pinia Colada; TanStack Query is a close second.
__Host-cookie (HttpOnly, Secure, SameSite=Strict), with nothing in Web Storage. Every*.rsl-commerce.testhost is same-site, so SameSite doesn't stop CSRF here. The backend needs an Origin/CORS allowlist plus a custom header or token. A flat permission set from/me, with route guards andv-if, is enough; CASL is not needed. The server enforces timeouts and step-up re-auth.useWebSocketwith backoff, jitter and a heartbeat, refetching on reconnect.Audit lists: manual pagination (
pageCount: -1), an infinite query and virtualization.Tests: Vitest 5 Browser Mode (Playwright provider) for components, Node Vitest for logic, MSW 3 and a thin Playwright E2E suite. Playwright component testing for Vue is discontinued.
nginx-unprivilegedalpine-slim: non-root, port 8080, about 8 MB compressed.• SPA fallback with
try_files $uri /index.html.• A header CSP with
script-src 'self'; style-src 'self'; frame-ancestors 'none'.•
/assets/*cachedimmutable;index.htmlsent withno-cache.Caddy runs as root by default and needs its admin API and auto-HTTPS turned off.
Recommendation
Framework: Vue 3.5 + Vite 8 + Vue Router 5 + TypeScript (F4-F10).
src/pages,src/features/<domain>,src/api) and require explicit imports.ssr: false: stronger conventions, but it brings 66 direct dependencies, telemetry and auto-imports (F6, F7), plus inline scripts that need a new CSP hash on every build (F8). That conflicts withAGENTS.md:29.Libraries
DataTablezod ^3.24, and Standard Schema is only in a stale v5 beta (F13). FormKit's own DOM and themes overlap with the design system (F15).echarts/core, withariaand decals on and charts lazy-loaded by routeCSSStyleSheet(), which works under a strict CSP (F19). Chart.js is about 5× smaller, but has no built-in accessibility and no release in 12 months (F20).Session and permissions (F24-F33)
Cookie: the backend sets
__Host-…; Secure; HttpOnly; SameSite=Strict; Path=/with no Domain. The SDK usescredentials: 'include'. Nothing goes in Web Storage or IndexedDB (F28, F29).CSRF, enforced by the backend:
Sec-Fetch-Site: same-siteis untrusted, because the storefront, checkout and docs hosts are sibling subdomains (F26, F27).Permissions:
/mereturns flat strings such asrefund:create;can()feeds onebeforeEachthat reads a typedmeta.permission, plusv-ifin templates;/me.CASL only pays off if the backend emits CASL rules with conditions (F30-F33).
Timeouts: the server enforces them, and the UI warns using
useIdleand the expiry from/me.reauth_required, the UI prompts the user, then retries with the sameIdempotency-Key.Real-time. An alerts store wraps
useWebSocket, withautoReconnect.delayas capped exponential backoff plus jitter, andheartbeaton. It invalidates queries on reconnect, because nothing replays missed events (F35, F40).Tests. Vitest 5 with two projects:
@vitest/browser-playwright+vitest-browser-vue), for components.MSW 3 handlers are shared with a thin Playwright E2E suite (F36-F39). The platform owns end-to-end tests across the whole ecosystem (F34).
@vue/test-utilswith happy-dom is faster but less faithful (F37). Avoid Playwright component testing (F38).Server and headers (F41-F48). nginx-unprivileged alpine-slim, read-only root filesystem.
/assets/:try_files $uri =404andpublic, max-age=31536000, immutable./:try_files $uri /index.htmlandno-cache.add_header_inherit merge, withalways):Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'self' https://api.rsl-commerce.test wss://api.rsl-commerce.test; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; object-src 'none';nosniff,strict-origin-when-cross-origin, COOPsame-origin, a deny-allPermissions-Policy, andX-Robots-Tag: noindex, nofollow.server_tokens offandbuild.assetsInlineLimit: 0. The API and WebSocket origins come from runtime config. HSTS is set at the platform proxy.Risks and unknowns
AGENTS.md:12). The backend spike decides it.api.rsl-commerce.test, and an XSS on any sibling host is same-site. The backend must isolate admin endpoints: a separate cookie name, per-route CORS and Origin checks, or an admin-only API host or path.style-src 'self'is unproven. The design system has no code yet. Start withReport-Only, and allow a looser policy for the dev server.What this means for the plan
Candidate decisions for the admin ADR:
DataTable; TanStack Form + Zod 4./me; no CASL.useWebSocketwith backoff, jitter, a heartbeat and a refetch on reconnect.Testable requirements:
unsafe-inlineorunsafe-evalin the CSP, and zero violations in E2E.Idempotency-Keyand handlereauth_required.The backend must provide:
GET /me, with roles, flat permissions, the idle and absolute expiry, and the last-authentication time.reauth_required, and a step-up endpoint.Idempotency-Keysupport and an audit record for every sensitive action.The platform must provide routing from
admin.rsl-commerce.testto port 8080, TLS and HSTS, and runtime API and WebSocket URLs.Verified by the lead before posting:
npm viewgives vue-router 5.3.1, pinia 4.0.3, @tanstack/vue-table 9.2.6, @tanstack/vue-form 1.33.5, zod 4.6.5, echarts 6.1.0, vue-echarts 8.3.1, chart.js 4.5.1, vitest 5.0.3, msw 3.0.2, @vueuse/core 15.0.0 and @hey-api/openapi-ts 0.99.0;unplugin-vue-routeris deprecated with "Merged into vuejs/router";@vee-validate/zod4.15.1 peerszod ^3.24.0;^22.12.0 || ^24.0.0 || >=26.0.0;Evidence
RQ-1: Framework
admin/README.md:8-16@a89f48b). The admin owns no components (:21) and uses the SDK plus WebSocket (:26-27).design-system/README.md:9-16@3e8fd6e), and holds "no application-specific components" (design-system/AGENTS.md:8-9@3e8fd6e).AGENTS.md:29-30@d0573a0)vue-router/viteplugin plustyped-router.d.ts. Data loaders are still experimental. (v4→v5 migration, accessed 2026-10-07, v5)ssr: false, Nuxt emitsindex.html,200.htmland404.html. nuxt@4.6.0 has 66 direct dependencies, among them the nitro server, devtools and telemetry. (Nuxt rendering; npm nuxt, accessed 2026-10-07, 4.6.0)__NUXT_DATA__andwindow.__NUXT__.config=scripts, and nuxt-security allows them by hash. (payload.ts @85b8d54 L22-49; nuxt-security, accessed 2026-10-07)cspNonceneeds per-request replacement. Small assets are inlined asdata:URLs (controlled byassetsInlineLimit), anddata:must never be allowed inscript-src. (Vite CSP; build options, accessed 2026-10-07, 8.3)RQ-2: Libraries
@tanstack/vue-table9.2.6 (9.0.0 shipped 2026-08-04) is headless: "100% control over the markup and styles". Virtualization comes from@tanstack/vue-virtual3.13.39. (quick start; virtualization, accessed 2026-10-07, v9)manualPagination, "If you don't know the page count, pass-1". A cursor recipe usesuseInfiniteQueryandnextCursor. (pagination, accessed 2026-10-07, v9)@vee-validate/zodpeerszod ^3.24. Standard Schema support exists only in v5.0.0-beta.0 (2025-08-02). (v5 beta; npm, accessed 2026-10-07)ariagenerates anaria-labeland decals, and it tree-shakes throughecharts/core. vue-echarts 8.3.1 injects its styles throughCSSStyleSheet(), so it works under a strict CSP. (ECharts aria; vue-echarts, accessed 2026-10-07)useInfiniteQuery;@tanstack/vue-query5.104.1 is 14.1 kB. (Pinia v4; Colada vs TanStack, accessed 2026-10-07)RQ-3: Session and permissions
AGENTS.md:21-22@d0573a0). "Hiding a button is not authorization", and sensitive actions go through audited operations (admin/AGENTS.md:8-9,15-17@a89f48b).rsl-commerce.testand itsapi.,admin.,checkout.anddocs.subdomains. (README.md:20-26@d0573a0)testis not on the 2026-10-06 list, so*applies. (MDN Site; PSL, accessed 2026-10-07)__Host-withSecure; HttpOnly; SameSite=Strict; Path=/. "Do not store … session IDs … in localStorage or sessionStorage". For high-value apps: 2-5 min idle and 4-8 h absolute, enforced server-side. (OWASP Session, accessed 2026-10-07)metais checked inbeforeEachand typed by augmentingRouteMeta. (Route Meta, accessed 2026-10-07, v5)@casl/vue3.0.1 providesabilitiesPlugin,useAbilityand<Can>. (casl-vue, accessed 2026-10-07)backend/README.md:1-20@38a8548)Sec-Fetch-Siteisolates the admin (F27); that leaves an Origin allowlist plus a header or token. A flat permission set is the least code that mirrors RBAC (F24, F30). CASL needs CASL rules from the backend (F31, F32).RQ-4: Real-time, audit views, tests
admin/README.md:15-16@a89f48b). The backend authorizes events into browser-safe ones (backend/AGENTS.md:13-14@38a8548). The platform owns end-to-end tests across the ecosystem (platform/README.md:17@5db9dab).useWebSockethasautoReconnect(a number or a function),heartbeatandstatus;useIdletracks inactivity. OWASP: "Validate theOriginheader on every handshake", close on session expiry, and keep tokens out of URLs. (useWebSocket; OWASP WebSocket, accessed 2026-10-07)vitest-browser-vue. (Vitest 4; Browser Mode; v5.0.0, accessed 2026-10-07)@vue/test-utilsand happy-dom. Browser runners catch "style issues, real native DOM events…" but are slower. (Vue testing, accessed 2026-10-07)experimental-ct-vuepackages "will no longer be updated"; they move to a stories model. (v1.63.0, 2026-09-04, accessed 2026-10-07)test.extendfixture. (v3.0.0; recipe, accessed 2026-10-07)RQ-5: Serving
platform/README.md:12@5db9dab), and provides the config each app declares (AGENTS.md:14@d0573a0)./tmp. It is rebuilt on each release, on critical CVEs and weekly. The 1.30.5 alpine-slim image is about 8 MB compressed. (README; Docker Hub tags, accessed 2026-10-07)add_headeris inherited "if and only if there are no add_header directives defined on the current level".add_header_inherit merge(1.29.3+) changes that, andserver_tokensdefaults toon. (headers module, accessed 2026-10-07)USER, so it runs as root, and it exposes port 2019 for its admin API.admin offandauto_https offare needed. (Dockerfile; options, accessed 2026-10-07)frame-ancestors,nosniff,strict-origin-when-cross-origin, COOP,Permissions-PolicyandX-Robots-Tag: noindex. A<meta>CSP can't setframe-ancestors. (Headers; CSP, accessed 2026-10-07)max-age=31536000, immutable; HTML getsno-cache. (MDN Cache-Control, accessed 2026-10-07)'self'CSP works without nonces. The checkout spike's experiment showed a Vite build rendering underscript-src 'self'(commerce-checkout#1, F17). nginx-unprivileged is non-root, small and patched (F42), while Caddy needs three changes (F44). nginx's trap is header inheritance (F43).Current state
The repo is docs-only:
admin/README.mdandadmin/AGENTS.md;actions(.github/workflows/codeql.yml:24);.github/dependabot.yml:8).Sources
All accessed 2026-10-07:
a89f48b, root @d0573a0, design-system @3e8fd6e, backend @38a8548and platform @5db9dab.No experiments were needed for this spike, and no repository files changed.
workflow · claude-code · 2026-10-07 00:39