Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Build context excludes — keep images lean and secret-free.

# deps & build outputs (installed/built fresh inside the image)
**/node_modules
**/.next
**/dist
**/coverage
**/*.tsbuildinfo
.turbo
**/.turbo

# vcs / editor / tooling
.git
.gitignore
.vscode
.claude
.github

# secrets & env — injected at RUNTIME via compose env_file; NEXT_PUBLIC_* via build-arg.
# Never bake .env files into an image layer.
**/.env
**/.env.*

# local-only docs (may contain prior brand text) — never ship in images
*.md
docs
VPS-*.md
CLAUDE.md
AGENTS.md

# screenshots / misc binaries
**/*.png
10 changes: 8 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ yarn-error.log*
.DS_Store
*.pem

# --- Forward-Mena additions ---
# --- Property Manager additions ---
# Harden env handling: ignore every env file except *.env.example
.env
.env.*
Expand All @@ -51,7 +51,7 @@ yarn-error.log*
id_*
*_rsa
*_ed25519
forward_mena_vps*
*_vps*

# Private / local-only docs — NEVER commit to the shared bootcamp repo
docs/
Expand All @@ -69,5 +69,11 @@ temporary.md
# Local issue tracking
issues/

# Local PM2 process manager config (machine-specific: ssh alias, ports)
ecosystem.config.js

# TypeScript incremental build info
*.tsbuildinfo

# Playwright MCP local artifacts (screenshots, traces)
.playwright-mcp/
58 changes: 28 additions & 30 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# AGENTS.md — Forward-Mena
# AGENTS.md — Property Manager

Instructions for AI coding assistants (Claude Code, Cursor, Codex, Aider, …). This is the source of truth for **how to write code here**. Read it before writing code; live architecture + status is in [docs/CONTEXT.md](docs/CONTEXT.md).

## What this repo is

**Forward-Mena** — a multi-org rental-management SaaS. Property owners subscribe ($20/mo) and manage their own staff (supervisor / finance / maintenance) and tenants in a fully data-isolated workspace. **v1** = platform foundation (auth, multi-org RBAC, subscription/payments, timeline, role-aware dashboard shells); the rentals domain is being built (`Building` / `BuildingAssignment` models exist). Turborepo monorepo.
**Property Manager** — a multi-org rental-management SaaS. Property owners subscribe (from $29/mo) and manage their own staff (supervisor / finance / maintenance) and tenants in a fully data-isolated workspace. **v1** = platform foundation (auth, multi-org RBAC, subscription/payments, timeline, role-aware dashboard shells); the rentals domain is being built (`Building` / `BuildingAssignment` models exist). Turborepo monorepo.

## Stack

- **`apps/web`** (`forward-mena-fe`) — Next.js **16.2.4** + React 19, App Router, bilingual `[lang]` routing (`ar` RTL / `en`), **next-auth v5 (Auth.js) with Keycloak OIDC** (JWT session, no DB adapter), **RTK Query + redux-persist**, Tailwind v4 + shadcn (on `@base-ui/react`), Stripe embedded checkout, **vitest**. **Port 3000.**
- **`apps/api`** (`forward-mena-be`) — NestJS **11**, **Prisma 7** (`@prisma/adapter-pg`), **passport-jwt + jwks-rsa** (validates Keycloak RS256 JWTs), Stripe 22, nestjs-pino, Swagger at `/docs`, `@nestjs/throttler`, **jest**. **Port 4000.**
- **`apps/web`** (`property-manager-fe`) — Next.js **16.2.4** + React 19, App Router, bilingual `[lang]` routing (`ar` RTL / `en`), **next-auth v5 (Auth.js) with Keycloak OIDC** (JWT session, no DB adapter), **RTK Query + redux-persist**, Tailwind v4 + shadcn (on `@base-ui/react`), Stripe embedded checkout, **vitest**. **Port 3000.**
- **`apps/api`** (`property-manager-be`) — NestJS **11**, **Prisma 7** (`@prisma/adapter-pg`), **passport-jwt + jwks-rsa** (validates Keycloak RS256 JWTs), Stripe 22, nestjs-pino, Swagger at `/docs`, `@nestjs/throttler`, **jest**. **Port 4000.**
- **`packages/`** — shared internal packages: **`@repo/db`** (`packages/database` — Prisma schema/migrations + generated client, the single source of truth for the DB), **`@repo/contracts`** (shared API types/DTOs consumed by both apps), plus **`@repo/typescript-config`** and **`@repo/eslint-config`**. New cross-app types/DTOs belong in `@repo/contracts`, not per-app.
- **Tooling** — **npm** (`npm@11.6.2`, `package-lock.json`), Node **24.11.1** (`.nvmrc`), Turborepo 2.

Expand Down Expand Up @@ -50,7 +50,7 @@ cd apps/web && npm run dev # Next.js :3000 → /en or /ar
- **Every BFF route handler under `app/api/` MUST use `forwardRoute('/path')`** (`lib/api/forward.ts`) — e.g. `export const GET = forwardRoute('/me')`. It wraps `auth(...)` so Auth.js writes the rotated Keycloak token back via `Set-Cookie`. A bare `await auth()` inside a handler refreshes in-memory only → eventual `invalid_grant`.
- `session.update()` MUST carry a payload (e.g. `update({ refresh: Date.now() })`) — a bare `update()` is a no-op that never triggers the `jwt` callback. (See memory: post-payment role refresh depended on this.)
- RSC guards `requireSession` / `requireRole` / `requireActiveOrg` (`auth/guards.ts`) are defense-in-depth. `proxy.ts` middleware handles locale + route gating; **paywall gating is intentionally NOT in middleware** (a lagging role claim causes redirect loops) — gate in RSC/components.
- Forms: react-hook-form + zod. UI: shadcn (base-ui) in `components/ui/` — **don't hand-edit**; use the shadcn MCP or CLI. Tailwind v4 tokens. State persisted via redux-persist (`ui`, `auth`, `api` under key `forward-mena`).
- Forms: react-hook-form + zod. UI: shadcn (base-ui) in `components/ui/` — **don't hand-edit**; use the shadcn MCP or CLI. Tailwind v4 tokens. State persisted via redux-persist (`ui`, `auth`, `api` under key `property-manager`).

### Billing (Stripe)

Expand Down Expand Up @@ -82,47 +82,45 @@ CI (`.github/workflows/ci.yml`, on PR + push to `main`) runs **lint + check-type
- Don't duplicate cross-app types per-app — put shared API types/DTOs in `@repo/contracts`, and import the DB client/types from `@repo/db` (never re-declare them).

<!-- gitnexus:start -->

# GitNexus — Code Intelligence

This project is indexed by GitNexus as **bootcamp-starter** (1530 symbols, 3956 relationships, 116 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **bootcamp-starter** (3700 symbols, 7360 relationships, 139 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.

> Index stale? Run `node .gitnexus/run.cjs analyze` from the project root — it auto-selects an available runner. No `.gitnexus/run.cjs` yet? `npx gitnexus analyze` (npm 11 crash → `npm i -g gitnexus`; #1939).
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

## Always Do

- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. For regression review, compare against the default branch: `detect_changes({scope: "compare", base_ref: "main"})`.
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
- When exploring unfamiliar code, use `query({search_query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `context({name: "symbolName"})`.
- For security review, `explain({target: "fileOrSymbol"})` lists taint findings (source→sink flows; needs `analyze --pdg`).
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.

## Never Do

- NEVER edit a function, class, or method without first running `impact` on it.
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
- NEVER rename symbols with find-and-replace — use `rename` which understands the call graph.
- NEVER commit changes without running `detect_changes()` to check affected scope.
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.

## Resources

| Resource | Use for |
| ------------------------------------------------- | ---------------------------------------- |
| `gitnexus://repo/bootcamp-starter/context` | Codebase overview, check index freshness |
| `gitnexus://repo/bootcamp-starter/clusters` | All functional areas |
| `gitnexus://repo/bootcamp-starter/processes` | All execution flows |
| `gitnexus://repo/bootcamp-starter/process/{name}` | Step-by-step execution trace |
| Resource | Use for |
|----------|---------|
| `gitnexus://repo/bootcamp-starter/context` | Codebase overview, check index freshness |
| `gitnexus://repo/bootcamp-starter/clusters` | All functional areas |
| `gitnexus://repo/bootcamp-starter/processes` | All execution flows |
| `gitnexus://repo/bootcamp-starter/process/{name}` | Step-by-step execution trace |

## CLI

| Task | Read this skill file |
| -------------------------------------------- | ----------------------------------------------------------- |
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
| Task | Read this skill file |
|------|---------------------|
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |

<!-- gitnexus:end -->
40 changes: 40 additions & 0 deletions apps/api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# syntax=docker/dockerfile:1
# Property Manager — API (NestJS) image. Build context = repo root.
# docker build -f apps/api/Dockerfile -t <reg>/property-manager-api:<tag> .

# ---------- builder ----------
FROM node:22-slim AS builder
# openssl → Prisma engine. (No node-gyp toolchain: the only native dep,
# msgpackr-extract, is optional and fails soft to a pure-JS path.)
RUN apt-get update && apt-get install -y --no-install-recommends \
openssl ca-certificates && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . .
RUN npm ci
# Build shared workspace packages in EXPLICIT order first. apps/* import
# @repo/contracts but don't all declare it as a dependency, so turbo's graph
# can race and build the app before contracts/dist exists. Build deps by hand.
RUN npm run build --workspace=@repo/contracts
RUN npm run db:generate --workspace=@repo/db
RUN npm run db:build --workspace=@repo/db
# API build: `npm run build` = rm tsbuildinfo && nest build.
RUN cd apps/api && npm run build
# Fail loudly if the Nest build did not emit.
RUN test -f apps/api/dist/main.js

# ---------- runtime ----------
FROM node:22-slim AS runtime
RUN apt-get update && apt-get install -y --no-install-recommends \
openssl ca-certificates && \
rm -rf /var/lib/apt/lists/*
ENV NODE_ENV=production
WORKDIR /app
# Copy the whole built monorepo. We intentionally keep devDependencies because
# `start:prod` registers `tsconfig-paths` (a devDependency) at runtime, and the
# generated Prisma client/engine lives under node_modules/.prisma.
COPY --from=builder /app ./
EXPOSE 20101
# start:prod = node -e "require('tsconfig-paths').register({baseUrl:'./dist',...}); require('./dist/main')"
# --workspace runs it with cwd = apps/api so the relative ./dist paths resolve.
CMD ["npm", "run", "start:prod", "--workspace=property-manager-be"]
18 changes: 14 additions & 4 deletions apps/api/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "forward-mena-be",
"name": "property-manager-be",
"version": "0.1.0",
"description": "Forward-Mena multi-org rentals SaaS backend",
"description": "Property Manager multi-org rentals SaaS backend",
"author": "",
"private": true,
"license": "UNLICENSED",
Expand All @@ -22,6 +22,7 @@
},
"dependencies": {
"@nestjs/axios": "^4.0.1",
"@nestjs/bullmq": "^11.0.4",
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.4",
"@nestjs/core": "^11.0.1",
Expand All @@ -33,6 +34,7 @@
"@prisma/adapter-pg": "^7.8.0",
"@repo/db": "*",
"axios": "^1.15.2",
"bullmq": "^5.80.6",
"class-transformer": "^0.5.1",
"class-validator": "^0.15.1",
"compression": "^1.8.1",
Expand All @@ -41,6 +43,7 @@
"joi": "^18.1.2",
"jwks-rsa": "^4.0.1",
"nestjs-pino": "^4.6.1",
"nodemailer": "^7.0.13",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.20.0",
Expand All @@ -63,6 +66,7 @@
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^24.0.0",
"@types/nodemailer": "^8.0.1",
"@types/passport-jwt": "^4.0.1",
"@types/pg": "^8.20.0",
"@types/supertest": "^7.0.0",
Expand All @@ -83,7 +87,11 @@
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": ["js", "json", "ts"],
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
Expand All @@ -92,7 +100,9 @@
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/$1"
},
"collectCoverageFrom": ["**/*.(t|j)s"],
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
Expand Down
Loading
Loading