# Plan 002 — ArchiTools thin-client migration: review feedback from eterra.live side **Date:** 2026-05-17 **Reviewer:** Claude session in `/home/orchestrator/Code/eterra-live` (Marius) **Context:** Reviews the "Faza 0..7" plan proposed by the architools claude that routes architools enrichment through eterra.live. --- ## TL;DR — change of direction The original plan made **eterra.live** the gateway for architools/planhub enrichment via `POST /api/internal/enrich`. **This is the wrong boundary.** The correct gateway is **`api.gis.ac`** (the `gis-api` service on shop), which is already designed for cross-app consumption (JWT + RLS + scope filtering, Hono on `10.10.10.84:3100`, fronted by Traefik). eterra.live is a single-instance Next.js end-user product. Putting architools' geoportal behind it makes eterra.live a SPOF — any deploy/restart breaks architools cadastre. The right model treats both architools AND eterra.live as **peer consumers** of api.gis.ac. eterra.live already migrated to api.gis.ac for `CfExtract` (Sprint 2 Day 3-4, 2026-04-20). Same migration pattern applies to architools, just bigger surface area. --- ## What's already in place (don't reinvent) `api.gis.ac` exists in production: - Repo: `gitadmin/gis-api`, local clone `~/Code/gis-api` - Stack: Next.js 16.1 + Hono 4 + Prisma 6.19 + Zod 4 + jose 6 - Auth: HS256 JWT today (`GIS_API_DEV_JWT_SECRET`) — **migrating to Authentik OIDC before architools cutover** (decision locked 2026-05-17) - Claims: `{ sub, org_ids[], is_beletage_group, enrichment_scope: "none"|"basic"|"full", email? }` — `tenant` claim being added - RLS: `withUserContext()` sets `app.user_id / org_ids / is_beletage_group / enrichment_scope` per Postgres tx - Live endpoints (under `/api/v1/`): - `GET /me` — claims echo - `GET /parcela/:id` — full `GisFeature` read - `GET /search?q=&limit=` — UAT + cadastralRef text search - `GET/POST/PATCH /enrichment/cf*` — CfExtract CRUD with RLS - `GET /enrichment/catalog/:nrCadastral` — catalog metadata - `GET/POST /enrichment/cf/:id/pdf` — MinIO PDF stream - RLS test suite: 6 scenarios, asserts BOTH HTTP and direct-SQL paths (CI: `.gitea/workflows/rls-test.yml`) What it does NOT have yet (gaps to close before architools cutover): 1. **Authentik OIDC** (HS256 still in use) 2. **Live-fetch proxy endpoints** to orchestrator (parcel/tech, parcel/units, building/tech, building/condo-owners, imm-apps) 3. **Scope-based field filtering** on `/parcela/:id` (currently leaks `enrichment.PROPRIETARI` regardless of scope) 4. **`tenant` claim** in JWT 5. **CORS allowlist** for cross-origin app calls 6. **Per-tenant rate limiting** 7. **Audit log table** (`gis_meta.api_audit`) These will be added in a dedicated gis-api session before architools starts consuming. Full spec is in gis-api project memory (`~/.claude/projects/-home-orchestrator-Code-gis-api/memory/`): - `project_architools_planhub_plan.md` — endpoints to add, in priority order - `feedback_jwt_authentik_decision.md` — Authentik OIDC spec (scope mapping from LDAP groups) - `reference_orchestrator_endpoints.md` — exact contracts for the 5 LAN endpoints to proxy - `reference_cross_app_architecture.md` — the diagram + consumer responsibility matrix --- ## Revised plan (replaces architools claude's Faza 0..7) ### Faza A — Wait for gis-api gaps to close (managed in gis-api repo, ~3 days) Driven from `~/Code/gis-api` in a separate session. NOT architools' work to do, but architools depends on it. Deliverables: 1. **PR1 (gis-api):** Authentik OIDC support alongside HS256 (`src/lib/auth.ts` issuer-based dual path). Add `tenant` claim. Eterra.live keeps working on HS256 during the overlap. Provisioning needed in Authentik: - New OAuth2 application + provider with slug `gis-api` - Property mappings for `enrichment_scope` derived from LDAP groups: - `beletage-staff` → `enrichment_scope=full`, `is_beletage_group=true` - `planhub-pro` → `full` - `planhub-free` / no match → `basic` - missing claim → REJECT - `tenant` claim from `aud` (application slug) 2. **PR2 (gis-api):** 5 proxy endpoints + scope filter on `/parcela/:id` + CORS allowlist: ``` POST /api/v1/parcel/tech → orchestrator POST /api/v1/parcel/tech POST /api/v1/parcel/units/fetch → orchestrator POST /api/v1/parcel-units/fetch POST /api/v1/parcel/imm-apps → orchestrator POST /api/v1/imm-apps POST /api/v1/building/tech → orchestrator POST /api/v1/building/tech POST /api/v1/building/condo-owners → orchestrator POST /api/v1/building/condo-owners ``` All require `enrichment_scope >= basic`. Rewrite `correlationId` server-side to `${tenant}:${user_sub[:8]}:${requestId}`. 3. **PR3 (gis-api):** Per-tenant rate limit + `gis_meta.api_audit` migration. When PR1+PR2 are green and deployed, signal back to this plan and proceed with Faza B. ### Faza B — ArchiTools secrets + Authentik client (½ day) - In Authentik: create OAuth2 client for architools (slug `architools-app`). Configure redirect URI for its NextAuth flow. - Infisical adds (prod env, path `/`): - `AUTHENTIK_CLIENT_ID` / `AUTHENTIK_CLIENT_SECRET` / `AUTHENTIK_ISSUER` / `AUTHENTIK_JWKS_URL` for architools - `GIS_API_URL=https://api.gis.ac` - `NEXT_PUBLIC_MARTIN_URL=https://tiles.gis.ac` - `NEXT_PUBLIC_PMTILES_URL=https://pmtiles.gis.ac/overview.pmtiles` - NextAuth on architools: add Authentik OIDC provider. Store `access_token` in session. ⚠️ DO NOT mint HS256 JWTs server-side anymore — pass the Authentik `access_token` directly as Bearer to gis-api. ### Faza C — Rip-out (1 day, ONE PR, feature-flagged) `NEXT_PUBLIC_USE_GIS_AC=1` flag controls cutover. With flag on: - Geoportal map sources flip to `pmtiles.gis.ac` + `tiles.gis.ac` - All `src/app/api/eterra/*` and `src/app/api/ancpi/*` route handlers replaced by thin wrappers that forward to api.gis.ac with the user's access_token - All `src/modules/parcel-sync/**` and parcel-sync UI removed - `src/config/{modules,navigation,flags}.ts` cleanup With flag off: old behavior retained. **Do not drop Prisma tables (GisFeature, GisUat, GisSyncRun, GisSyncRule, CfExtract) in this PR.** Keep them as dead columns for 2-3 days post-cutover so rollback stays cheap. Drop in a follow-up PR after validation in prod. Stop `martin` container on satra ONLY after flag-on is verified working in prod. The PMTiles webhook on satra:9876 — **does not exist**; the architools claude's plan had this wrong. The actual PMTiles trigger is the orchestrator cron at 03:00 EEST + admin button in eterra.live, hitting `gis-tippecanoe-builder` on **shop** at port 9876. ### Faza D — Thin client (1-2 days) - `src/lib/gis-api-client.ts` — fetch wrapper with the Authentik access_token from session - `gisApi.parcela.get(id)` → `GET /api/v1/parcela/:id` - `gisApi.search(q, limit)` → `GET /api/v1/search` - `gisApi.parcel.tech({siruta, cadastralRef, force})` → `POST /api/v1/parcel/tech` - `gisApi.parcel.unitsFetch(...)` → `POST /api/v1/parcel/units/fetch` - `gisApi.parcel.immApps(...)` → `POST /api/v1/parcel/imm-apps` - `gisApi.building.tech(...)` → `POST /api/v1/building/tech` - `gisApi.building.condoOwners(...)` → `POST /api/v1/building/condo-owners` - `gisApi.enrichment.cf.list/get/create/patch/uploadPdf/getPdf/getCatalog` (mirror eterra.live's existing usage) - NO separate `scope-mapper.ts` — gis-api resolves scope from Authentik claims itself. - NO separate `gis-api-token.ts` — Authentik access_token is the JWT, NextAuth session already has it. ### Faza E — Geoportal rewrite (2 days) - `map-viewer.tsx`: sources `pmtiles://pmtiles.gis.ac/overview.pmtiles` + Martin `tiles.gis.ac/{view}/{z}/{x}/{y}`. Drop satra Martin entirely. - `search-bar.tsx` → `gisApi.search()`. - `feature-info-panel.tsx` → `gisApi.parcela.get()` + (on missing enrichment) `gisApi.parcel.tech()` + `gisApi.parcel.unitsFetch()` for buildings. - `basemap-switcher.tsx`: same config as eterra.live (liberty/dark/satellite/orto/topo50). Source files are public CSS/tile URLs; literal copy is OK. - Decision for `boundary-check`, `cf-status`, `export`, `pad`, `piz`: if architools keeps them, rewrite as thin proxies to api.gis.ac equivalents (which may need new endpoints — defer that scoping until each is actually needed). ### Faza F — ePay / CF ordering (½ day) - Architools UI for "Comandă extras CF" calls `gisApi.enrichment.cf.create({nrCadastral, type, ...})` directly. - Status + download list: `gisApi.enrichment.cf.list({...})` and `getPdf(id)`. - The `tenant=architools` claim in the access_token tells gis-api which billing account to attribute the order to (future-work for billing routing, but the field is in place from Faza A). - Delete `src/app/api/ancpi/*` entirely. ### Faza G — Test E2E + deploy (½ day) - `/geoportal` in architools renders tiles from gis.ac, click parcel → enrichment via api.gis.ac → live-fetch fallback triggers orchestrator. - Verify zero references to `architools_postgres.GisFeature` remain in code AND that no DB query goes to the deprecated tables. - Type-check + lint + build clean. - Commit + push + Portainer redeploy. - 24h grace period: old stack (Martin satra, deprecated parcel-sync code) kept around. After validation, follow-up PR drops Prisma tables + stops martin satra container. ### Faza H — Planhub (separate PR, post-architools) - Same pattern. Different scope mapping: `planhub-free` → `basic`, `planhub-pro` → `full`. - Already covered by gis-api's tenant claim from Faza A. --- ## Specific corrections to the original plan 1. **`POST /api/internal/enrich` on eterra.live** — REMOVE. Use `api.gis.ac` directly. 2. **Bearer `$ARCHITOOLS_INTERNAL_TOKEN` static token** — REMOVE. Use Authentik OIDC access_token. 3. **`GIS_API_JWT_SECRET (shared)` in Infisical** — REMOVE. With Authentik OIDC, gis-api fetches JWKS from `https://auth.beletage.ro/application/o/gis-api/jwks/` — no shared secrets between apps. 4. **`scope-mapper.ts` in architools** — REMOVE. Scope is in the JWT claims; architools doesn't compute it. 5. **PMTiles webhook satra:9876** — does not exist. The builder is `gis-tippecanoe-builder` on **shop:9876**, fired by orchestrator cron + eterra.live admin button. 6. **Single-PR rip-out** — replace with feature-flagged migration (`NEXT_PUBLIC_USE_GIS_AC=1`), 2-3 day overlap, drop dead code in a separate follow-up PR after prod validation. 7. **eterralive-coordinator** terminology — drop. The pool is "owned" by `gis-sync-orchestrator` (LAN-only on shop:3101). gis-api is the auth/scope/audit gateway in front of it. --- ## Open questions 1. **Authentik OIDC client provisioning** — needs to be done first; until then, no architools migration. Marius or a sys-admin session needs to set up the OAuth2 provider in Authentik UI. 2. **Audit table schema** — does `gis_meta` already have an `api_audit` table? Probably no. Migration needed in same PR as proxy endpoints. 3. **Rate-limit defaults** — what's the cap per tenant? 100 req/min seems safe; configurable per tenant via Infisical JSON env. Confirm with Marius. 4. **Architools' `archi.*` schema writes** — currently architools writes Canvas, Job, RegistryAudit, RgiSearchTemplate directly via its own Prisma. Migration here is OPTIONAL for the cadastre work; could be done in a later sprint. Don't bundle it. --- ## Reference docs - gis-api project memory: `~/.claude/projects/-home-orchestrator-Code-gis-api/memory/MEMORY.md` - Plan 001 v2 master: `~/Code/ArchiTools/docs/plans/001-v2-central-gis-db-multi-app.md` - Tile data sources / cache / gis.ac rule: `~/Code/busc-infra/references/TILE-DATA-SOURCES.md` - Orchestrator workers + schema: `~/Code/busc-infra/references/SPRINT3.md` - GIS stack infrastructure: `~/Code/busc-infra/references/GIS-STACK.md` - Authentik configuration: `~/Code/busc-infra/references/AUTHENTIK.md` - Sprint 2 (gis-api delivery): `~/Code/busc-infra/references/SPRINT2.md`