A modern frontend service template built on the current toolchain:
- Vite+ 1.4 (
vp) — one binary for dev, build (Vite 8 + Rolldown), test (Vitest 5), lint (Oxlint) and format (Oxfmt); all configured invite.config.ts - shadcn/ui
base-novastyle on Base UI (@base-ui/react1.9), scaffolded withbunx --bun shadcn@latest init --preset b0 --template vite --base base - Tailwind CSS v4 (CSS-first, theme tokens in
src/index.css, notailwind.config.js) - React 19 + TypeScript strict, Redux Toolkit 2.13 (+
redux-persist), RTK Query, Apollo Client 4 (GraphQL), Keycloak 26 via a small own provider, react-router 7, i18next 26, react-hook-form + zod 4, TanStack Table v9, MSW 3 - shadcn-ui/lint (
@shadcn/lint) design-system rules running inside Oxlint — seevite.config.tslint.rules
| Command | Description |
|---|---|
bun run dev |
Vite+ dev server on http://localhost:3000 |
bun run build |
tsc -b + production build (vp build) |
bun run preview |
Serve the production build |
bun run test |
Vitest once (vp test) |
bun run coverage |
Vitest with V8 coverage |
bun run check |
vp check — format + lint + typecheck in one pass |
bun run lint / bun run format |
Oxlint / Oxfmt individually |
bun run typecheck |
tsc --noEmit |
bun run generate:graphql |
Regenerate src/graphql/generated/ from src/graphql/schema.graphql |
Run a single test: bun run test -- src/App.test.tsx (or -t "name").
Start the backend stack first (spring-reactive-mongo-template dev-infra), then bun run dev.
| Dependency | URL |
|---|---|
Keycloak (realm app, client frontend) |
http://localhost:8090 |
Backend REST /coroutine/*, GraphQL /graphql, SSE /update/user |
http://localhost:8080 |
Login via the user menu (top right). Real credentials live in the git-ignored .credentials file — copy .credentials.template and fill it in on a fresh clone. URLs come from the committed .env (all variables documented in .env.template; .env.production is generated by CI from repo secrets).
Live updates (/users) subscribe to GET /update/user with a bearer header — the client streams via fetch (no EventSource, no ticket, no credential in the URL). Reconnects always resolve a fresh token first (getAccessToken()), token_expired → refresh + reconnect, logged_out → the local session is expired, protected pages lock immediately and the stream never reconnects (see src/hooks/useSseUsers.ts).
src/
├── components/ # shared chrome: nav, side-menu, user-dropdown, cookie-consent,
│ │ # mode-toggle, loading-screen, spinner, error-boundary
│ ├── permissions/ # header widget: permission CRUD in a Sheet via Apollo GraphQL;
│ │ # usePermissionsGraphql.ts is the app's sole permission data layer
│ └── ui/ # generated shadcn (base-nova) — do not hand-edit
├── graphql/ # schema.graphql + generated/ (codegen client preset; git-ignored)
├── lib/ # keycloak.ts, apollo.ts, utils (cn re-export)
├── locales/ # i18next init + en_US / pl_PL
├── pages/ # feature folders with colocated api/components/hooks/tests
│ ├── counter/ # RTK slice demo
│ ├── home/ # landing card
│ ├── error/ # 401 / 404 pages
│ └── users/ # RTK Query + SSE CRUD; owns its data-table,
│ # roles combobox (data from components/permissions) and SSE status bar
├── providers/ # keycloak.tsx (own Keycloak context), theme.tsx (light/dark/system)
└── store/ # store + persisted counter slice + RTK Query api
Generated into src/components/ui/ via bunx --bun shadcn@latest add <name> (registry: shadcn Base UI base-nova, preset b0). Never hand-edit; re-add or diff to update. This is the authoritative list:
| Component | Used by |
|---|---|
badge |
sse-status-bar |
button |
everywhere (nav, dialogs, table, counter, error-boundary…) |
card |
home, counter, users, error-boundary |
combobox |
roles-combobox (chips multi-select) |
command |
— (scaffold default, currently unused) |
dialog |
user-dialog |
dropdown-menu |
user-dropdown-menu, mode-toggle, table row actions |
field |
user-dialog form layout (the form component is a stub in this preset — use field + react-hook-form + zod) |
input-group |
App header search; also a dependency of combobox |
input |
App search, user-dialog, counter |
label |
internally by field |
navigation-menu |
App header, nav-item |
popover |
— (installed, currently unused) |
select |
data-table page-size picker |
separator |
internally by field |
sheet |
mobile side menu |
sonner |
App <Toaster> + all toasts |
table |
data-table |
textarea |
internally by input-group |
Add a component with bunx --bun shadcn@latest add <name> and update this table in the same change.
Dockerfile builds with oven/bun:1.4-slim (bun install → generate:graphql → vp build) and serves dist/ with nginx:1.30-alpine-slim (SPA fallback, hashed-asset caching, /health). Multi-arch (amd64/arm64) publishing runs in .github/workflows/docker.yml on main/release/* → docker.io/<org>/<repo>:latest.
Build-time config: VITE_APP_* are inlined from .env.production (CI creates it from repo secrets via SpicyPizza/create-envfile) — the deployed origin must be registered in the Keycloak client's redirect/web origins.
docker build -t react-typescript-vite-plus-template .
docker run --rm -p 3000:80 react-typescript-vite-plus-template.github/workflows/ci.yml— every push/PR: install → codegen →vp check(format + lint + typecheck) → tests → build.github/workflows/docker.yml—main/release/*: tests first, then multi-arch image publish with GHA layer cache
bun run generate:graphqlmust run beforebuild/testafter cloning (generated code is git-ignored).- Formatting is Oxfmt (double quotes, 80 cols) —
vp check --fixnormalizes everything; re-rungenerate:graphqlafter formatting GraphQL document template literals. src/components/ui/**is excluded from the shadcn design-system lint rules (components own their appearance); every other source file (src/components/*,src/providers,src/pages/**,src/lib) is fully linted.vp testuses thetestblock invite.config.ts(jsdom + globals +src/setupTests.ts).
MIT — see LICENSE.md.