Spanish version: README.es.md
SEOTracker is a full-stack monorepo for running, scheduling and comparing SEO audits against your own sites. It is split into a NestJS HTTP API, a unified BullMQ worker/scheduler service, and a TanStack Start frontend, all built on top of a shared backend runtime and a typed-domain shared package.
seotracker/
├── apps/
│ ├── api/ # NestJS HTTP entrypoint (REST /api/v1, Swagger, auth)
│ ├── worker/ # BullMQ processors + cron scheduler
│ └── web/ # TanStack Start + React + Tailwind v4 frontend
├── packages/
│ ├── server/ # Shared backend runtime (Drizzle schema, Nest modules, queue, lock)
│ ├── shared-types/ # Enums + DTOs shared by backend and frontend
│ └── config-typescript/ # Shared TS preset
├── infra/
│ ├── docker/ # Dockerfiles + docker-compose dev stack
│ ├── proxy/ # Reverse-proxy config
│ └── railway/ # Railway deploy notes
├── scripts/ # Repo-level helper scripts (e.g. git hooks bootstrap)
├── .github/workflows/ # CI + dependency review
├── package.json, pnpm-workspace.yaml, turbo.json
├── oxlint.config.ts, oxfmt.config.ts
└── README.md
Each subdirectory has its own README.md with details.
- Backend: NestJS 11, Drizzle ORM (PostgreSQL), BullMQ (Redis), pino logging, Argon2 passwords, JWT access + rotating refresh tokens, CSRF double-submit, Helmet.
- Frontend: TanStack Start (React + Nitro SSR), TanStack Router, TanStack Query, Zustand, Tailwind v4.
- Tooling: pnpm workspaces + Turborepo, oxlint + oxfmt + Ultracite presets, simple-git-hooks, Jest, Vitest, GitHub Actions CI.
The frontend is in Spanish (the target audience is Spanish-speaking). Code, comments, JSDoc and commit messages are in English.
- Node.js 22+
- pnpm 11.0.8 (use Corepack:
corepack enable && corepack prepare pnpm@11.0.8 --activate) - Docker (for the local Postgres/Redis/Mailhog stack)
git clone <repo-url>
cd seotracker
pnpm install
# Set up env files (copy and fill the placeholders)
cp apps/api/.env.example apps/api/.env
cp apps/worker/.env.example apps/worker/.env
cp apps/web/.env.example apps/web/.env
# Generate JWT secrets and paste them into BOTH apps/api/.env AND apps/worker/.env
# (the worker signs/verifies the same tokens as the API, so the secrets must match)
openssl rand -base64 48 # → JWT_ACCESS_SECRET
openssl rand -base64 48 # → JWT_REFRESH_SECRET
# Start local infrastructure
docker compose -f infra/docker/docker-compose.yml up -d postgres redis mailhog
# Apply migrations up front (recommended; the API also checks them at boot)
pnpm db:migrate
# Run every workspace in dev mode
pnpm dev| Service | URL |
|---|---|
| API | http://localhost:4000/api/v1 |
| Swagger UI | http://localhost:4000/docs |
| Web | http://localhost:3000 |
| Mailhog | http://localhost:8025 |
| Postgres | localhost:5432 (postgres / postgres / seotracker) |
| Redis | localhost:6379 |
pnpm dev # turbo run dev
pnpm build
pnpm lint
pnpm typecheck
pnpm test
pnpm format
pnpm format:check
pnpm check
pnpm verify # format:check + lint + typecheck + test + build
pnpm db:generate # drizzle-kit generate (apps/api)
pnpm db:migrate # drizzle-kit migrate (apps/api)
pnpm db:studio # drizzle-kit studioSEOTracker includes an internal SEO-engine telemetry system. Every audit records per-stage duration, status and diagnostic details in audit_engine_telemetry; platform administrators can inspect audit waterfalls and aggregate engine health from the web UI (/engine-health, site-level engine health) or the API (/api/v1/engine-health*). Access is gated by PLATFORM_ADMIN_EMAILS.
The backend also ships a score-calibration benchmark corpus with 216 public websites in packages/server/scripts/score-calibration-domains.txt. Run it with pnpm --filter @seotracker/server score:calibrate and optionally compare against Google PageSpeed/Lighthouse SEO scores with --with-pagespeed.
The monorepo uses oxlint + oxfmt with Ultracite presets.
- Root config:
oxlint.config.ts,oxfmt.config.ts. - Package-level scripts are reserved for build, dev, test and typecheck. Linting and formatting run from the root.
pnpm format # rewrite files with oxfmt
pnpm lint # oxlint across the monorepo
pnpm check # Ultracite aggregate check
pnpm fix # apply Ultracite autofixes
pnpm verify # full pre-push checksimple-git-hooks is configured at the root:
pre-commit:pnpm format:check && pnpm lintpre-push:pnpm verify
Hooks are installed via the root prepare script the first time you run pnpm install.
GitHub Actions runs on pull requests and pushes to the main branch:
pnpm verify(format check, lint, typecheck, test, build)- Dependency review on pull requests
See .github/workflows/.
Migrations are owned by the API workspace and live in apps/api/drizzle/. The API applies pending migrations during bootstrap as a safety net; running them explicitly before starting/scaling services is still recommended:
pnpm db:migrateTo create a new migration after editing packages/server/src/database/schema.ts:
pnpm db:generateInspect data with the Drizzle Studio:
pnpm db:studio- Port already in use — change the port in the relevant
.env(PORT=for API,vite dev --portfor web) or stop the conflicting process. docker compose upfails with "port is already allocated" — a local Postgres/Redis is binding 5432/6379. Stop it (e.g.brew services stop postgresql redis) or remap the host ports ininfra/docker/docker-compose.ymland updateDATABASE_URL/REDIS_URLin the.envfiles accordingly.docker compose upfails for other reasons — make sure Docker Desktop is running and that ports 5432/6379/1025/8025 are free.- API refuses to boot with "JWT secret looks like a placeholder" — generate real secrets with
openssl rand -base64 48and updateapps/api/.env. The validator rejects values starting withchange-this,__replace_me__orreplace-me. - Frontend hits 401 in a redirect loop — the dev proxy must be reachable; make sure the API is up at
http://localhost:4000and thatapps/web/.envmatches the API'sCSRF_COOKIE_NAME. - Pre-commit hook says nothing changed but lint still fails — run
pnpm fixonce to apply autofixes, then re-stage the changes.