- 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 001 — DB GIS Central Multi-App
Status: DRAFT v1 — neaprobat, de discutat Autor: Claude + Marius Tarau Data: 2026-04-18 Scope: Arhitectură unificată GIS pentru ArchiTools, eTerra.live, SmartCity360, Planhub ERP + servicii viitoare
1. Context & motivație
Date GIS (cadastru ANCPI, enrichment, PUG/PUZ, UTR) sunt folosite de multiple platforme:
- ArchiTools — portal intern Beletage (există, producție)
- eTerra.live — SaaS topografi (proxy eTerra, repo nou gitadmin/eterra-live)
- SmartCity360 — portal GIS primării (în arhitectură)
- Planhub ERP — ERP arhitecți cu geoportal (în arhitectură)
- Viitoare servicii
Nevoie: single source of truth GIS cu acces controlat, sync centralizat, cache/CDN partajat, izolare multi-tenant.
2. Topologie
All on-prem. Nimic în cloud public.
- Servere: satra (10.10.10.166), shop (10.10.10.84), proxy (10.10.10.199), orchi (10.10.40.60 — CJ2 via VPN)
- Gateway public: Traefik pe proxy, Sophos DNAT 80/443
- Cloudflare doar pentru domenii SaaS public-facing (eTerra.live, SmartCity360)
3. DB Central — Shop server (10.10.10.84)
Resurse disponibile (verificat 2026-04-18):
- Xeon Gold 6430, 128 vCPU, 251GB RAM, 3.5TB NVMe (1% ocupat)
- Rulează deja: Supabase stack, WordPress, Tegola
- Load avg ~0.5, headroom masiv
Cluster Postgres nou, dedicat:
- Container
postgres-gis(separat de Supabase existent — izolare critică) - Postgres 16 + PostGIS 3.4 + pg_cron
- SRID nativ 3844 (Stereo70)
- Port intern, expus doar pe 10.10.10.0/24 + VPN CJ2 (10.10.40.0/24)
- NICIODATĂ expus direct pe internet
Schema split
| Schema | Conținut | Access |
|---|---|---|
gis_core |
Cadastru național ANCPI: UAT, terenuri, clădiri, administrativ | Public read, write doar sync daemon |
gis_urban |
PUG, PUZ, PUD, UTR, zone protecție, zone reglementate | Read public (layer public), write per tenant primărie (RLS) |
gis_enrichment |
CF, proprietari, adrese, categorii folosință — GDPR sensibil | Auth obligatoriu, RLS pe rol |
gis_meta |
Sync runs, rules, status, audit | Internal only |
tenants |
Primării, firme arh, clienți SaaS | Control plane |
Roluri Postgres
gis_sync_rw— write pegis_core+gis_meta(sync daemons)gis_public_ro— read pegis_core+ layer public dingis_urban(Martin public tiles)gis_app_ro— read authenticated cu RLS pegis_urbanprivat +gis_enrichmentgis_urban_rw— write pegis_urbandoar pentru tenant-ul propriu
RLS pe multi-tenant
-- exemplu pentru gis_urban tabele private
CREATE POLICY tenant_isolation ON gis_urban.puz_private
USING (siruta = ANY(current_setting('app.allowed_sirutas', true)::text[]));
4. Layer acces — public/auth endpoints
| Subdomain | Serviciu | Auth | Scop |
|---|---|---|---|
tiles.beletage.ro |
Martin (instanță dedicată) | Niciun auth pe layer public | Tile server public cadastru + PUG public |
pmtiles.beletage.ro |
nginx static | Niciun auth | Overview Romania, PMTiles pre-generated |
gis-api.beletage.ro |
Next.js API sau PostgREST | JWT Authentik | Enrichment, query spațial, mutations |
sync.beletage.ro |
Orchestrator UI | Authentik admin | Control panel sync central |
eterra.live |
SaaS topografi | Authentik | Portal topografi |
planhub.beletage.ro |
ERP arhitecți | Authentik | Geoportal + PM |
<primarie>.smartcity360.ro |
Portal primărie | Cetățeni anonim + admin Authentik | Subdomain per tenant |
Traefik — reverse proxy unic, SSL auto. Cloudflare în față pe domenii SaaS public (eTerra.live, SmartCity360) cu rate limit + CDN cache tile-uri (TTL 24h pentru UAT).
5. ACL Enrichment (per rol)
| Rol | Vede enrichment |
|---|---|
topograf (eTerra.live) |
Full — CF, proprietari, adrese |
architect (Planhub ERP) |
Full + PUG complet |
admin_primaria (SmartCity360) |
Full pentru UAT-ul primăriei + date PUG private |
cetatean (SmartCity360 public) |
Doar nr. cadastral + suprafață + UTR |
admin (ArchiTools) |
Tot |
Implementat prin RLS + current_setting('app.role').
6. Shared enrichment pool — insight cheie
De-duplicare globală. Dacă topograf A cere enrichment pe parcela X:
- Salvat în
gis_enrichmentcentral - Topograf B cere aceeași parcelă → hit instant, zero request la eTerra
- TTL default 90 zile → după expire auto-refresh
Beneficii:
- Economii masive de sesiuni eTerra
- Viteză maximă pentru request-uri repetate
- Baza de date crește cu datele "cele mai cerute" de topografi (natural prioritization)
TBD: TTL fix 90 zile vs decizie manuală a topografului pentru "force refresh"?
7. Sync workers — centralizat
Extrage din ArchiTools într-un serviciu standalone gis-sync-orchestrator:
- Un singur scheduler (nu duplicat per app)
- Workers:
eterra-sync— delta sync cadastru (nocturn automat + on-demand per UAT)enrichment-worker— pulls CF/owners pe cererepug-ingest(viitor) — import PUG din shapefiles/PDFs
- Queue:
pg-boss(Postgres-backed, zero infra extra) sau BullMQ+Redis - Delta sync folosește
gis_uats.last_updated_dtmdeja existent - Notificare apps la sync complete:
Postgres LISTEN/NOTIFY+ webhook → fiecare app se abonează - PMTiles rebuild trigger-uit de orchestrator după sync major (webhook existent satra:9876)
Session pool eTerra per-user
- Tabel
eterra_sessionskeyed byuser_id(fiecare topograf = sesiune proprie) - Sync on-demand eTerra.live folosește credențialele topografului logat
- Sync nocturn automat folosește cont service (actualul hardcoded)
- Avantaj: toți topografii + sistemul automat contribuie la menținerea DB-ului actual
8. Consumers
| App | Schemas | Write | DB user | Deploy |
|---|---|---|---|---|
| ArchiTools | core + urban + enrichment | enrichment | archi_rw |
satra |
| eTerra.live | core + enrichment (ACL topograf) | enrichment via sync | eterra_ro + eterra_sync |
satra/shop TBD |
| SmartCity360 | core + urban (filter siruta) | urban (PUG/PUZ) | sc_rw |
TBD |
| Planhub ERP | core + urban | proiecte proprii (schemă erp) |
erp_rw |
TBD |
Fiecare app:
- Own Authentik OIDC app
- Own DB user + permisiuni minimale
- Own MinIO bucket
- Own Infisical project pentru secrete
9. Securitate
- DB niciodată expus direct pe internet → tot prin Martin / API cu auth
- Enrichment (CF, proprietari) → NU în tile-uri publice, doar via API auth'd
- Rate limit Cloudflare pe
tiles.*(ex: 100 req/s per IP) - Audit log pe
gis_urbanîngis_meta.audit— cine modifică PUG primăriei - Secrete în Infisical, proiecte separate per app (zero reuse)
- Backup:
pg_basebackup+ WAL archiving zilnic → MinIO satra + replică async CJ2 (PITR 14 zile)
10. Performanță
- Tabele simplificate pe zoom (deja există pentru UAT: z0/5/8/12) — extinde pe
gis_terenuridacă nevoie - Materialized views pentru query-uri grele cross-tenant (refresh nocturn)
- Indexe GIST pe toate
geom, BRIN pecreated_atpentru audit - Martin pool: 16 conexiuni per instanță, 2 instanțe behind Traefik LB
- Read replica Postgres când QPS > 500/s (streaming replication)
- PMTiles pentru tot ce nu se schimbă zilnic (UAT, administrativ) — mult mai rapid decât Martin live
11. Scale target an 1
- ~50 arhitecți (Planhub ERP)
- ~100 topografi (eTerra.live)
- ~2 primării × 5 admin = 10 admin_primaria (SmartCity360)
- Citizen public access (SmartCity360) — nedeterminat
Concluzie: Shop server (128 vCPU, 251GB RAM) = zero probleme la scale-ul ăsta. Rezerve pentru 10x creștere.
12. Migrare graduală — fără downtime ArchiTools
- Setup
postgres-giscontainer pe shop, config de bază - Replicare initial snapshot
architools_db→postgres-gis(pg_dump + restore) - Creează schemas noi (
gis_urban,tenants), mută tabele existente (ALTER TABLE SET SCHEMA) - Creează roluri + RLS policies, testează cu user nou
- Extrage sync în serviciu standalone — paritate funcțională cu ArchiTools
- Lansează Martin dedicat pe
tiles.beletage.ro - ArchiTools trece pe endpoint nou (feature flag)
- Verify paritate + performanță → cutover final
- eTerra.live + SmartCity360 + Planhub ERP conectate la DB central
13. Date GIS existente (autoritative)
| Tabel | Conținut | Features aprox |
|---|---|---|
gis_uats |
Limite UAT Romania (+ views z0/5/8/12 simplified) | 3181 UAT-uri |
gis_administrativ |
Intravilan + arii speciale ANCPI | — |
gis_terenuri |
Parcele cadastrale eTerra | Cluj 774K (extrapolare ~25M total Romania) |
gis_cladiri |
Clădiri cadastrale eTerra | Cluj — (similar magnitude) |
gis_terenuri_status / gis_cladiri_status |
Parcele/clădiri + flag enrichment/legal | — |
gis_features |
Raw eTerra (attributes JSON + enrichment JSON) | — |
gis_sync_rules, gis_sync_runs |
Orchestrare sync | — |
Status actual: Cluj complet sincronizat (81 UAT, ~8.3h), restul României de populat (estimare 15-25h inițial, 1-2h delta).
14. Date GIS de adăugat pentru SmartCity360 + ERP
Urbanism PUG:
gis_urban.utr— Unități Teritoriale Referință (code, zone_type, regulation JSON cu POT/CUT/H_max, geom)gis_urban.zone_reglementate— zone funcționale PUGgis_urban.zone_protectie— monumente, arheologie, peisaj, sanitară, hidrologică
PUZ/PUD:
gis_urban.puz/gis_urban.pud— doc_ref, HCL_number, approval_date, status, geom perimetru
Administrativ suplimentar:
gis_urban.strazigis_urban.numerotaregis_urban.dotari_publice(școli, spitale, parcuri)
Fază 2 (opțional):
- Rețele utilități (apă, canal, gaz, electric, telecom)
- Cadastru verde (arbori, scuaruri)
15. Întrebări deschise — TBD
Tehnice
-
Supabase vs Postgres raw pe shop — schemă nouă în Supabase existent (câștig: RLS auto, PostgREST gratis, auth) sau container Postgres separat (izolare totală, simplu)? Recomandare curentă: separat.
-
Domain SaaS —
eterra.livedeja deținut?smartcity360.rode cumpărat? Subdomain per primărie (cluj-napoca.smartcity360.ro) vs path-based (smartcity360.ro/cluj-napoca)? -
Portal cetățeni SmartCity — acces complet anonim (zero login, doar rate limit) vs email/captcha pentru abuse prevention?
-
TTL enrichment — 90 zile auto-refresh implicit? Topograf poate forța refresh manual?
-
Backup offsite — replică Postgres async la CJ2 (10.10.40.x) via VPN? Sau doar MinIO snapshot zilnic?
-
Migrare ArchiTools — DB-ul actual rămâne paralel cu noul central, apoi cutover? Sau migrare directă cu downtime controlat?
-
Martin shared vs per-app — o singură instanță Martin publică pentru toate app-urile, sau instanțe separate per app (branding URL + izolare)?
Business / organizaționale
-
Billing / metering — eTerra.live SaaS plătit: how measured? Per sync on-demand? Per enrichment request? Per user/lună?
-
Data ownership — dacă topograf X face enrichment pe parcela Y, datele sunt ale lui privat sau contribuie automat la pool-ul central public? (răspuns parțial: shared pool, dar GDPR?)
-
PUG private pentru primării — ce date specific sunt "private" vs "public"? (ex: observații interne, documentație anexă)
-
SLA public tiles — uptime target? Recovery time după outage?
16. Concluzii curente
- Arhitectura e fezabilă pe infrastructura existentă fără hardware nou
- Shop server = candidat ideal DB central
- Shared enrichment pool = insight major cu ROI imediat
- Migrarea se poate face gradual fără downtime
- Securitate + multi-tenancy rezolvate cu RLS + Authentik per-app
Next steps când e aprobat:
- Răspuns la întrebările deschise (secțiunea 15)
- Design detaliat schema
gis_urban(DDL concret) - Design API
gis-api.beletage.ro(endpoints + auth flow) - Plan migrare ArchiTools pas-cu-pas
- POC postgres-gis pe shop + test Martin + 1 app consumer