harden(epay): cart-hygiene invariant uses confirmed cart count + add service architecture plan
- 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>
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user