- cartCount tracks actual cart rows (decrement only on confirmed delete) so a failed cleanup delete can't trigger a false dirty-cart abort. - docs/plans/006: the multi-tenant CF-service architecture (DB-backed fulfiller, account pool, catalog dedup, per-tenant credential model, reversible flag flip) — the executable next phase. The Phase-F flag flip is gated on the orchestrator fulfiller existing (Plan 003 Faza F was wrong). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 KiB
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? }—tenantclaim being added - RLS:
withUserContext()setsapp.user_id / org_ids / is_beletage_group / enrichment_scopeper Postgres tx - Live endpoints (under
/api/v1/):GET /me— claims echoGET /parcela/:id— fullGisFeaturereadGET /search?q=&limit=— UAT + cadastralRef text searchGET/POST/PATCH /enrichment/cf*— CfExtract CRUD with RLSGET /enrichment/catalog/:nrCadastral— catalog metadataGET/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):
- Authentik OIDC (HS256 still in use)
- Live-fetch proxy endpoints to orchestrator (parcel/tech, parcel/units, building/tech, building/condo-owners, imm-apps)
- Scope-based field filtering on
/parcela/:id(currently leaksenrichment.PROPRIETARIregardless of scope) tenantclaim in JWT- CORS allowlist for cross-origin app calls
- Per-tenant rate limiting
- 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 orderfeedback_jwt_authentik_decision.md— Authentik OIDC spec (scope mapping from LDAP groups)reference_orchestrator_endpoints.md— exact contracts for the 5 LAN endpoints to proxyreference_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:
- PR1 (gis-api): Authentik OIDC support alongside HS256 (
src/lib/auth.tsissuer-based dual path). Addtenantclaim. 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_scopederived from LDAP groups:beletage-staff→enrichment_scope=full,is_beletage_group=trueplanhub-pro→fullplanhub-free/ no match →basic- missing claim → REJECT
tenantclaim fromaud(application slug)
- New OAuth2 application + provider with slug
- PR2 (gis-api): 5 proxy endpoints + scope filter on
/parcela/:id+ CORS allowlist:All requirePOST /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-ownersenrichment_scope >= basic. RewritecorrelationIdserver-side to${tenant}:${user_sub[:8]}:${requestId}. - PR3 (gis-api): Per-tenant rate limit +
gis_meta.api_auditmigration.
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_URLfor architoolsGIS_API_URL=https://api.gis.acNEXT_PUBLIC_MARTIN_URL=https://tiles.gis.acNEXT_PUBLIC_PMTILES_URL=https://pmtiles.gis.ac/overview.pmtiles
- NextAuth on architools: add Authentik OIDC provider. Store
access_tokenin session. ⚠️ DO NOT mint HS256 JWTs server-side anymore — pass the Authentikaccess_tokendirectly 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/*andsrc/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}.tscleanup
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 sessiongisApi.parcela.get(id)→GET /api/v1/parcela/:idgisApi.search(q, limit)→GET /api/v1/searchgisApi.parcel.tech({siruta, cadastralRef, force})→POST /api/v1/parcel/techgisApi.parcel.unitsFetch(...)→POST /api/v1/parcel/units/fetchgisApi.parcel.immApps(...)→POST /api/v1/parcel/imm-appsgisApi.building.tech(...)→POST /api/v1/building/techgisApi.building.condoOwners(...)→POST /api/v1/building/condo-ownersgisApi.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: sourcespmtiles://pmtiles.gis.ac/overview.pmtiles+ Martintiles.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({...})andgetPdf(id). - The
tenant=architoolsclaim 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)
/geoportalin architools renders tiles from gis.ac, click parcel → enrichment via api.gis.ac → live-fetch fallback triggers orchestrator.- Verify zero references to
architools_postgres.GisFeatureremain 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
POST /api/internal/enrichon eterra.live — REMOVE. Useapi.gis.acdirectly.- Bearer
$ARCHITOOLS_INTERNAL_TOKENstatic token — REMOVE. Use Authentik OIDC access_token. GIS_API_JWT_SECRET (shared)in Infisical — REMOVE. With Authentik OIDC, gis-api fetches JWKS fromhttps://auth.beletage.ro/application/o/gis-api/jwks/— no shared secrets between apps.scope-mapper.tsin architools — REMOVE. Scope is in the JWT claims; architools doesn't compute it.- PMTiles webhook satra:9876 — does not exist. The builder is
gis-tippecanoe-builderon shop:9876, fired by orchestrator cron + eterra.live admin button. - 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. - 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
- 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.
- Audit table schema — does
gis_metaalready have anapi_audittable? Probably no. Migration needed in same PR as proxy endpoints. - Rate-limit defaults — what's the cap per tenant? 100 req/min seems safe; configurable per tenant via Infisical JSON env. Confirm with Marius.
- 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