28c870fb12
- 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>
286 lines
12 KiB
Markdown
286 lines
12 KiB
Markdown
# 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 pe `gis_core` + `gis_meta` (sync daemons)
|
||
- `gis_public_ro` — read pe `gis_core` + layer public din `gis_urban` (Martin public tiles)
|
||
- `gis_app_ro` — read authenticated cu RLS pe `gis_urban` privat + `gis_enrichment`
|
||
- `gis_urban_rw` — write pe `gis_urban` doar pentru tenant-ul propriu
|
||
|
||
### RLS pe multi-tenant
|
||
|
||
```sql
|
||
-- 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:
|
||
1. Salvat în `gis_enrichment` central
|
||
2. Topograf B cere aceeași parcelă → **hit instant**, zero request la eTerra
|
||
3. 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 cerere
|
||
- `pug-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_dtm` deja 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_sessions` keyed by `user_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` în `gis_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_terenuri` dacă nevoie
|
||
- **Materialized views** pentru query-uri grele cross-tenant (refresh nocturn)
|
||
- **Indexe GIST** pe toate `geom`, **BRIN** pe `created_at` pentru 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
|
||
|
||
1. Setup `postgres-gis` container pe shop, config de bază
|
||
2. Replicare initial snapshot `architools_db` → `postgres-gis` (pg_dump + restore)
|
||
3. Creează schemas noi (`gis_urban`, `tenants`), mută tabele existente (`ALTER TABLE SET SCHEMA`)
|
||
4. Creează roluri + RLS policies, testează cu user nou
|
||
5. Extrage sync în serviciu standalone — paritate funcțională cu ArchiTools
|
||
6. Lansează Martin dedicat pe `tiles.beletage.ro`
|
||
7. ArchiTools trece pe endpoint nou (feature flag)
|
||
8. Verify paritate + performanță → cutover final
|
||
9. 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 PUG
|
||
- `gis_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.strazi`
|
||
- `gis_urban.numerotare`
|
||
- `gis_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
|
||
|
||
1. **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.
|
||
|
||
2. **Domain SaaS** — `eterra.live` deja deținut? `smartcity360.ro` de cumpărat? Subdomain per primărie (`cluj-napoca.smartcity360.ro`) vs path-based (`smartcity360.ro/cluj-napoca`)?
|
||
|
||
3. **Portal cetățeni SmartCity** — acces complet anonim (zero login, doar rate limit) vs email/captcha pentru abuse prevention?
|
||
|
||
4. **TTL enrichment** — 90 zile auto-refresh implicit? Topograf poate forța refresh manual?
|
||
|
||
5. **Backup offsite** — replică Postgres async la CJ2 (10.10.40.x) via VPN? Sau doar MinIO snapshot zilnic?
|
||
|
||
6. **Migrare ArchiTools** — DB-ul actual rămâne paralel cu noul central, apoi cutover? Sau migrare directă cu downtime controlat?
|
||
|
||
7. **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
|
||
|
||
8. **Billing / metering** — eTerra.live SaaS plătit: how measured? Per sync on-demand? Per enrichment request? Per user/lună?
|
||
|
||
9. **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?)
|
||
|
||
10. **PUG private pentru primării** — ce date specific sunt "private" vs "public"? (ex: observații interne, documentație anexă)
|
||
|
||
11. **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:**
|
||
1. Răspuns la întrebările deschise (secțiunea 15)
|
||
2. Design detaliat schema `gis_urban` (DDL concret)
|
||
3. Design API `gis-api.beletage.ro` (endpoints + auth flow)
|
||
4. Plan migrare ArchiTools pas-cu-pas
|
||
5. POC postgres-gis pe shop + test Martin + 1 app consumer
|