This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
VHF e Superiori is a website/community platform for an amateur radio (ham radio) association. It's a pnpm monorepo with three packages:
backend/— Express + TypeScript + MongoDB (Typegoose) REST APIfrontend/— React 18 (JS/JSX, Vite, Tailwind, Flowbite, Material Tailwind)frontend/server/— Express server (vite-express) that serves the built frontend in production
Run from the repo root using pnpm workspace filters.
pnpm install # install all workspace deps
pnpm dev # run backend + frontend + frontend-server in parallel
pnpm dev:backend # backend only (nodemon + ts-node, watches src/)
pnpm dev:frontend # frontend only (vite dev server, port 3000, proxies /api -> :6961)
pnpm dev:frontend-server # frontend-server only
pnpm build # build all packages
pnpm build:backend # tsc compile backend to backend/build
pnpm build:frontend # vite build, outputs to frontend/server/dist
pnpm serve-frontend # build frontend then start frontend-server (production-style)
pnpm start:backend # build then run backend/build/index.js
pnpm start:frontend-server # run frontend-server (expects frontend already built)
pnpm check # biome check --fix (lint + format, whole repo)
pnpm check:staged # biome check --staged (used by husky pre-commit)There is no test suite configured (no test script in any package, no test files). supertest/ts-node are present as devDependencies in the backend but unused for tests currently.
Linting/formatting is done with Biome (biome.json at repo root) — it covers JS/TS/JSX/CSS/HTML. Pre-commit runs pnpm check:staged via husky. Notable rule choices: double quotes, noNonNullAssertion off, useImportType off, useUniqueElementIds/noNestedComponentDefinitions/noUndeclaredDependencies off, noUndeclaredVariables error.
The backend requires ffmpeg, libvips-dev, and imagemagick system packages (used for media/QSL card processing).
index.ts→ loads dotenv and imports./api.api/index.tsis the real entrypoint: importsshared(env validation + logger),db(mongoose connection),auth,config,mailjet, then callscreateServer()fromapi/utils/server.tsand starts listening onIP:PORT.api/utils/server.tsmounts/api→apiRoutes, serves Swagger docs at/api-docsoutside production, and returns a 404 JSON error for everything else.api/utils/apiRoutes.tsis the central router: sets up morgan logging (viaLoggerStream/logger), body-parser, cookie-parser,populateUser(attachesreq.userfrom JWT),impersonate(admin "log in as another user" viax-impersonate-userheader), andexpress-fileupload(100MB limit, max 7 files per field, 300MB hard cap, temp files underBASE_TEMP_DIR). It then mounts each feature router under/api/<feature>.shared/envs.tsvalidates all required env vars at startup withenvalid(process exits if any are missing) — see this file for the full list of required configuration (Mongo, JWT/cookie secrets, QRZ, Mailjet/SMTP, AWS S3, Google Maps, Telegram bot, Turnstile captcha, temp dirs, etc).
Each feature follows the same shape:
models/— Typegoose classes (@modelOptions/@propdecorators) with embedded@swaggerJSDoc component schemasschemas/—express-validatorSchemaobjects (createSchema.ts,updateSchema.ts) used with the sharedvalidatehelper; sanitization (e.g. DOMPurify via jsdom for HTML fields) happens hereroutes/— one file per HTTP verb/operation (get.ts,all.ts,create.ts,update.ts,delete.ts,upload.ts...), each exporting an ExpressRouter, composed inroutes/index.tswith appropriate middleware (e.g.isAdminfor mutating routes)- Routes use
@openapiJSDoc comments for Swagger generation (api/docs/specs.ts)
New features should be wired into api/utils/apiRoutes.ts under /api/<name>.
- JWT-based auth via passport (
passport-jwt,passport-local), populated ontoreq.userbypopulateUsermiddleware. - Role/flag checks live in
api/middlewares/:isAdmin,isVerified,isDev,isLoggedIn. TheUserClassmodel (api/auth/models/User.ts) has boolean flags likeisAdmin,isVerified/etc — check this model for the full set of roles before adding new permission checks. impersonatemiddleware lets an admin act as another user via thex-impersonate-userrequest header (set automatically by the frontend axios interceptor when impersonation is active).- Errors are centralized in
api/errors/errors.ts(anErrorsenum/object) and returned via thecreateError()helper ({ err, ...extra }shape) with appropriatehttp-statuscodes.
api/jobs/— cron jobs (cronpackage), registered via side-effect import (import "../jobs") inapiRoutes.tsapi/mailjet/,api/email/,api/telegram/— outbound notification channels (Mailjet for transactional/marketing email, SMTP via nodemailer, Telegram bot for error/admin alerts)api/aws/— S3 client/presigner for file storageapi/qrz/,api/adif/,api/qso/,api/eqsl/— amateur radio specific domain logic (QRZ.com lookups, ADIF log import/export, QSO records, eQSL card generation)api/map/,api/location/— geo/maps features (Google Maps API, geolib)api/backup/— mongodump-based backup routes
- Entry:
main.jsx→index.jsx, which sets upBrowserRouter/Routes(react-router v7), i18next, GA4 page tracking, react-cookie, and a Material TailwindThemeProvider. - Top-level layout/shared components live directly under
src/:App.jsx(providesEventsContext,JoinOpenContext,ViewsContext,SidebarOpenContext),Layout.jsx,Header.jsx,Footer.jsx,SplashLoader.jsx/Splash.jsx. - Feature folders (each roughly maps to a section of the site and a backend feature):
admin/,auth/,event/,blog/,social/,beacon/,profile/,map/,document/,antenna/. stores/— Zustand stores (plain.js, withpersistmiddleware), notablyuserStore.js(auth state:userisfalsewhile loading,nullif unauthenticated, or the user object; also handles impersonation state). An axios request interceptor inindex.jsxreadsimpersonatedUserIdfrom this store and sets thex-impersonate-userheader on every request.shared/— reusable components/utilities used across features (map containers, formatting helpers, loading placeholders, etc).i18n/— i18next setup (multi-language support, language detector + HTTP backend).- Vite dev server proxies
/api/*to the backend athttp://localhost:6961(seefrontend/vite.config.js); production build output goes tofrontend/server/distand is served byfrontend/server(vite-express + helmet). - Code is mostly
.jsx/.js(not TypeScript), styled with Tailwind CSS + Flowbite/Material Tailwind components.