Source: apps/docs/content/architecture.mdEdit this page

Nutqi — Architecture

How Nutqi is put together: the applications and shared packages, the technology stack, platform conventions, and the bilingual/RTL strategy.

System at a glance#

Rendering diagram…

Monorepo layout#

nutqi/
  apps/
    web/    Next.js 15 App Router (ar RTL default + en), Tailwind v4, next-intl
    api/    NestJS 11 + Prisma + PostgreSQL 16, REST /api/v1
    docs/   this documentation site (statically rendered markdown)
  packages/
    ui/     design tokens + shared React components (Button, Card, StatusChip, …)
    shared/ TypeScript types + enums + zod schemas shared web↔api
  specs/         implementation specs (source of truth for the build)
  docs-content/  markdown sources rendered by the docs site

One pnpm workspace orchestrated by Turborepo, so every app and package builds, lints, and tests through the same pipeline.

Stack#

LayerChoice
WebNext.js 15 (App Router), Tailwind CSS v4, next-intl — Arabic (RTL) default, English secondary
APINestJS 11, REST under /api/v1, validated DTOs, bilingual error messages
DataPostgreSQL 16 + Prisma; cuid IDs; audit columns on every table
AuthSecure httpOnly cookie sessions; role-based access control plus row-level ownership checks
ThemingCSS custom properties in @nutqi/ui; light and dark, light the default
DocsNext.js 15 static site rendering the repo's markdown; Mermaid diagrams

Platform conventions#

  • Money is stored as integer piasters (EGP × 100) and formatted by a shared utility — no floating-point currency anywhere.
  • Timestamps are stored in UTC and rendered in Africa/Cairo.
  • Soft delete (deletedAt) preserves notes, plan templates, work-info entries, and job postings.
  • Every table carries createdAt / updatedAt; IDs are cuid.
  • File uploads are served through the API (/api/v1/files/:id) with the storage layer abstracted for cloud object storage.
  • Realtime: server-sent events feed the in-app notifications badge.

Bilingual & RTL#

  • Arabic is the default locale and renders right-to-left; English is the secondary locale, left-to-right. Validation and API error messages are bilingual (message + messageAr, honoring Accept-Language).
  • Layouts use logical CSS properties throughout, so every component mirrors automatically when the direction flips.
  • This documentation ships as two complete, unmixed editions — Arabic (RTL) and English (LTR) — so a reader never meets both scripts in the same sentence. Direction comes from <html dir> and every block inherits it; blocks are deliberately not left to detect their own direction, because that resolves from the first strong character and so flips an Arabic paragraph that happens to open with an identifier or a digit. Only runs of the foreign script are isolated — Latin inside Arabic prose, Arabic inside English prose — which keeps the punctuation around them attached to the host sentence. The Glossary is the single page where the two scripts appear side by side, in separate columns, and its table cells are the one place per-cell direction detection is still used.

Theme#

  • Two themes, light and dark, with light as the default: a first-time visitor always gets light, even on an operating system set to dark. prefers-color-scheme is deliberately never auto-applied — only an explicit choice switches the theme.
  • The active theme is one attribute on the root element (data-theme), and the choice is remembered per device in browser storage. A small script applies it before the first paint, so a returning dark-mode reader never sees a white flash.
  • Colour lives in one semantic token layer: the brand ramps never change between themes, and only the meanings built on top of them — page background, surfaces, borders, accent text, status-chip tones, chart series, elevation — flip. Components reference the meanings, never a raw colour, which is what lets a single attribute repaint the whole product.
  • Both the product and this documentation site use the same attribute and the same stored preference, so switching in one is recognised by the other.

Security posture#

  • Browser: an enforcing Content-Security-Policy carrying a fresh nonce per request, so only scripts the server vouched for execute; unsafe-eval is never granted. Alongside it: HSTS, cross-origin isolation headers, nosniff, a deny-framing policy, and no framework banner.
  • API: every request body is validated strictly and unknown fields are rejected outright, body size is capped, and requests are rate-limited per IP with a much tighter budget on the sign-in and OTP routes. API documentation is off in production unless explicitly enabled.
  • Boot: a missing signing secret or an empty allowed-origins list refuses to start in production rather than falling back to an insecure default, so a misconfigured deploy fails loudly instead of running open.
  • Health: GET /api/v1/health serves the reverse proxy and the container healthcheck — liveness plus a real database check, and deliberately free of version numbers, connection strings and error text.

Deployment#

  • The web app and this documentation site deploy to Vercel from the Git repository: a preview URL per pull request, production on main.
  • The API runs on a Docker host behind Nginx rather than a serverless platform, because it holds long-lived database connections, writes uploaded files to disk, and runs migrations on release. The kit lives in deploy/: a Compose stack (API + PostgreSQL, health checks, a one-shot migration step), an Nginx TLS template, provisioning, deploy and rollback scripts, and a runbook covering backups, restores and certificate renewal.
  • The API port binds to localhost only; Nginx is the sole public surface.
For developers

Local development (portless)#

Every dev server starts through portless, which assigns clean local HTTPS hostnames and injects PORT — no script hardcodes a port (e.g. portless nutqi-docs -- pnpm --filter @nutqi/docs dev).

ServiceHostnameNotes
Webhttps://nutqi-web.localhostNext.js dev server
APIhttps://nutqi-api.localhostSwagger UI at /api/docs
Docshttps://nutqi-docs.localhostthis site
  • The web app keeps browser calls same-origin through a Next.js rewrite: /api/:path*${API_URL}/api/:path*, so httpOnly cookie auth works across the separate dev hostnames. API_URL is wired from portless get nutqi-api.
  • The API sets CORS_ORIGIN to include the web hostname.
  • Databases: nutqi_dev for development, nutqi_test for tests.
  • A build never disturbs a running dev server. Both Next apps pin the dev server to its own output directory (NEXT_DIST_DIR=.next-dev in the dev script) and leave .next to builds. Sharing one directory meant a pnpm build overwrote the chunks a live dev server was serving, and every subsequent request failed with MODULE_NOT_FOUND on a webpack chunk — which reads like a code bug but is not. Separating dev rather than build keeps the build path byte-identical for CI and Vercel, so the safe behaviour needs no flag to remember.

CI pipeline (GitHub Actions)#

.github/workflows/ci.yml runs on pushes to main and on every pull request (superseded runs are cancelled). Seven jobs run in parallel, each declaring the minimum permissions it needs and carrying a timeout:

  1. Gates — pnpm + Node with dependency and Turborepo caches, a PostgreSQL 16 service container prepared by prisma migrate deploy, then linttypechecktestbuild, each fanning out across every workspace package.
  2. Secrets — repository secret scanning.
  3. Dependencies — vulnerability audit and a licence policy check.
  4. Static analysis — code scanning.
  5. Dependency review — on every pull request.
  6. Container — builds the API image, emits a software bill of materials, and fails on high or critical vulnerabilities.
  7. Migrations — catches schema drift before it reaches an environment.

A production deploy job runs only on main, after the gates pass. Preview deployments come from the Vercel Git integration, so every pull request has a URL a reviewer can click.

The same four gates are expected to pass locally at the repo root before pushing.

Quality gates beyond the four#

Several invariants that reviewers cannot reliably eyeball are enforced as scripts and tests instead:

GateEnforces
check-logical.mjsNo physical-direction utilities (pl/pr, ml/mr, left/right) in web or UI sources, so RTL mirrors automatically
check-i18n.mjsar/en key parity per namespace, no empty values, no Arabic left in an English string, no untranslated copy-paste
check-literal-colors.mjsNo literal palette colours in components — everything resolves through the semantic token layer, or dark mode silently breaks
no-mixing.test.tsEach documentation edition is single-script, with the Glossary asserted as the deliberate exception
direction.test.tsOnly foreign-script runs are isolated, and code subtrees are left alone
smoke.test.tsHeading structure matches across editions, and the decisions log carries the same decision ids in the same order

How this docs site works#

  • apps/docs is a fully static Next.js 15 app. At build time it reads the markdown straight from docs-content/ and specs/02-api.md — the sources are never copied or forked, so the repo markdown remains the single source of truth.
  • Diagrams are authored as mermaid code fences and rendered to SVG in the browser by Mermaid 11 (client component, loaded on demand).
  • The search index is generated at build time from the same sources and served as static JSON.
  • The Architecture page (apps/docs/content/architecture.md) and this appendix are the only content authored inside the docs app itself.