- 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>
28 KiB
Plan 001 v2 — DB GIS Central Multi-App
Status: FINAL APPROVED — 2026-04-20 (replaces v1 DRAFT) Autor: Marius Tarau + Claude Code Scope: Arhitectură unificată GIS pentru ArchiTools, eterra.live, pug.digital, planhub.ro + viitoare servicii Execution trigger: user sends "go" in new session → orchestrator begins Sprint 1
0. Cum recunoaște Claude "GO"
În sesiunea următoare, la cuvântul "go" singular:
- Re-read
/home/orchestrator/Code/ArchiTools/docs/plans/001-v2-central-gis-db-multi-app.mdcomplet - Read
project_plan001_GO_sequence.mddin memorii - Verifică secrete pre-loaded în Infisical (listă la §20)
- Pornește Sprint 1 — Day 1 (§16)
1. Executive summary
Ce construim: platformă GIS centralizată pe server shop (10.10.10.84) care servește 4 produse (ArchiTools intern, eterra.live SaaS topografi, pug.digital SaaS primării, planhub.ro SaaS arhitecți) + viitoare servicii.
Durată: 5 săptămâni dev + 1 weekend cutover + 1 săptămână cleanup = ~7 săptămâni total.
Echipă: 1 dev (Marius) + Claude Code pair.
Rezultat:
- DB unică (PostgreSQL 18.3 + PostGIS 3.6.3) schemă split, RLS multi-tenant
- Tile serving unificat (Martin + TiTiler + PMTiles + Cloudflare CDN)
- Sync orchestrator cu multi-account ANCPI shuffle
- Backup multi-tier (NAS + B2 + viitor Azure + viitor CJ2)
- Dashboard unificat admin eterra.live
- MDLPA export compliance pentru PUG/PUZ/PUD/PMUD
2. State curent (baseline)
Infrastructură
- Shop (10.10.10.84): Xeon Gold 6430, 128 vCPU, 251GB RAM, 3.3TB NVMe free
- Satra (10.10.10.166): Ubuntu, 194GB disk (61% plin), architools_postgres curent
- Proxy (10.10.10.199): Traefik v3.6.8
- NAS NewAmun (10.10.10.10): NETGEAR ReadyNAS, 40.88TB free, btrfs
App-uri curente
- ArchiTools (tools.beletage.ro) — Next.js, Prisma, conectat architools_postgres satra
- eterra.live — Next.js v5 NextAuth, per-user AES-256-GCM, același DB
- Martin tile server v1.4.0 pe satra port 3010
POC postgres-gis (2026-04-19)
- Deployed pe shop la
/home/dnz/postgres-gis/ - PG 18.3 + PostGIS 3.6.3 + pgBouncer v1.24.1-p1
architools_dbrestored from satra: 16GB, 24 tabele, 100% paritate row counts- Performance test: 9.4× faster ST_Intersects vs satra
- Status: RUNNING sandbox, zero impact production
Securitate rotate 2026-04-19
- AUTHENTIK_CLIENT_SECRET ✅
- NEXTAUTH_SECRET ✅
- DB_PASS Postgres ✅
- MINIO_ACCESS/SECRET_KEY ✅ (user nou gen-hex)
- NOTIFICATION_CRON_SECRET ✅
- ETERRA_USERNAME/PASSWORD ✅ (mutate Infisical)
Backup infrastructure testată 2026-04-20
- NAS rsync via SSH key RSYNC-ONLY flag,
gisbkp/→/HDD/gisbkp(symlink), 50 MB/s LAN - Backblaze B2 bucket
beletage-gis-backups(EU, Object Lock Compliance, SSE-B2), key scoped
3. Decizii arhitecturale (finalizate)
Golden rule (aplicat întreg planul)
Preferă investment upfront pentru câștiguri pe termen lung în: safety, speed, resources, future-proof.
Tehnologii
- PostgreSQL 18.3 (async I/O, skip scans, temporal constraints, UUID v7)
- PostGIS 3.6.3 (ST_RemoveIrrelevantPointsForView, SFCGAL modern)
- pgBouncer 1.24.1-p1 (transaction mode, 1000 max clients, 50 default pool)
- Martin 1.5.0 tile server (vector MVT)
- TiTiler (raster COG)
- Next.js 16 + Hono + Prisma + Zod pentru gis-api
- pg-boss queue (NU Redis)
- pgBackRest backup engine
- Docker Compose (NU Kubernetes)
- MinIO object storage
- Authentik OIDC (existent)
Versioning policy
- Latest stable ≥3 luni în producție + ≥1 patch
- Pin exact în compose (NU
:latest) - Manual upgrades după test pe snapshot
- Periodic review trimestrial
Domains (brand strategy)
| Domain | Rol |
|---|---|
gis.ac |
Infra neutru — tiles, api, pmtiles, s3 |
eterra.live |
SaaS topografi |
pug.digital |
SaaS primării (fost SmartCity360) |
planhub.ro |
SaaS multi-tenant arhitecți |
app.beletage.ro |
Beletage tenant CNAME rebrand |
vreau.digital |
Rezervă — portal cetățeni |
buildini.ai |
Rezervă — AI design |
puz.digital |
SEO redirect → pug.digital |
beletage.ro |
Intern + admin |
4. Target stack shop (13 containere)
shop (10.10.10.84) — /home/dnz/postgres-gis/ + /opt/gis-stack/:
├── postgres-gis PG 18.3 + PostGIS 3.6.3 [✅ POC]
├── postgres-gis-pgbouncer transaction mode, 1000 clients [✅ POC]
├── martin-public vector tiles publice, zero auth [Sprint 1]
├── martin-private vector tiles auth via JWT ForwardAuth [Sprint 1]
├── titiler raster COG + DEM proxy [Sprint 1]
├── gis-api Next.js 16 + Hono + Prisma + Zod [Sprint 2]
├── gis-sync-orchestrator pg-boss workers + session pool [Sprint 3]
├── nginx-pmtiles static serve MinIO pmtiles [Sprint 1]
├── minio-gis buckets: pmtiles, cog, cf-pdfs, dxf, backups [Sprint 1]
├── pgadmin intern doar (pgadmin.beletage.ro) [Sprint 1]
├── prometheus scrape metrics [Sprint 4]
├── grafana dashboards [Sprint 4]
└── alertmanager alerts → n8n webhook [Sprint 4]
Resource utilization estimat: ~8% CPU shop, ~15% RAM shop. Loaded la plural, idle majority.
5. Schema DB split
postgres-gis / gis (database):
├── schema: gis_core Cadastru ANCPI (public readable)
│ ├── terenuri Parcele (25M estimate)
│ ├── cladiri Clădiri cadastrale
│ ├── uats 3,186 UAT România
│ ├── administrativ Intravilan + arii protejate
│ └── uats_z0/5/8/12 Simplified views per zoom
│
├── schema: gis_urban PUG/PUZ/PUD (multi-tenant RLS per siruta)
│ ├── plan_spatial MDLPA-compliant root (SIRUTA, Judet, HCL, stadiu)
│ ├── zf_existenta, zf_propusa Zone funcționale + HILUCS codes
│ ├── zona_reglementare Zone protecție (monumente, sanitară, etc.)
│ ├── utr Unități Teritoriale Referință + regulament JSONB
│ ├── puz, pud Sub-plans
│ ├── cai_comunicatie Drumuri
│ ├── retele_edilitare Apă, canal, gaz, electric, telecom
│ ├── echipare_edilitara Hidranți, stații (puncte)
│ ├── regulament_local Text regulament
│ ├── avize_acorduri PDF metadata
│ ├── *_drafts Versiuni work-in-progress arhitecți
│ └── puz_in_avizare Public preview în timpul avizării
│
├── schema: gis_enrichment GDPR sensitive (CF, proprietari, adrese)
│ ├── cf_extracts Criptat, RLS strict
│ ├── proprietari Nume + CNP
│ ├── adrese Normalizare nomenclator
│ └── shared_pool GDPR-safe shareable (nr_cad, suprafață, UTR)
│
├── schema: gis_meta Orchestrare + audit
│ ├── sync_runs Log sync eTerra
│ ├── sync_rules Planning sync
│ ├── audit ENCRYPTED cu pgcrypto (audit metadata)
│ ├── eterra_sessions Session pool persistent
│ ├── eterra_accounts Multi-account ANCPI shuffle
│ ├── eterra_account_usage_log Per-action audit
│ └── raster_sources COG registry (upload metadata)
│
├── schema: tenants Control plane multi-tenancy
│ ├── tenants id, name, tenant_type, is_beletage_group, siruta_scope[]
│ ├── members user → tenant mapping
│ └── enrichment_scopes per-user flag (none/basic/full)
│
├── schema: eterra App-specific eterra.live
├── schema: pug App-specific pug.digital
├── schema: archi App-specific ArchiTools
├── schema: planhub App-specific planhub.ro (multi-tenant)
├── schema: queue pg-boss queue
└── schema: public Extensii (postgis, pg_stat_statements, pgcrypto, pg_trgm)
Super-tenant Beletage group
Tabela tenants.is_beletage_group BOOLEAN. Tenants marked (Beletage SRL, Studii de Teren, Urban Switch, Cubitron, + viitoare firme asociate) au acces NERESTRICȚIONAT la toate datele. Controlat prin admin Beletage.
6. Roluri Postgres + RLS
DB users
gis_app_rw— gis-api maingis_sync_rw— orchestrator worker (UNICUL scriitor în gis_core)gis_public_ro— Martin public (read-only)gis_private_ro— Martin-private (read-only cu RLS)gis_titiler_ro— TiTiler rastergis_admin_dba— pgAdmin + Marius (NO BYPASSRLS — Golden)
Session variables setate la fiecare tranzacție
SET LOCAL app.user_id = ...;
SET LOCAL app.tenant_id = ...;
SET LOCAL app.is_beletage_group = 'true'|'false';
SET LOCAL app.enrichment_scope = 'none'|'basic'|'full';
SET LOCAL app.allowed_sirutas = 'csv,list';
SET LOCAL app.roles = 'csv,list';
RLS policy patterns
gis_core: zero RLS, public readgis_urban.*_drafts: tenant-isolated (own tenant_id OR Beletage group)gis_urban.*_aprobate: public read, write admin_primaria sau Beletagegis_enrichment.cf_extracts:USING ( current_setting('app.is_beletage_group', true)::boolean = true OR fetched_by_user_id = current_setting('app.user_id', true)::uuid OR fetched_by_tenant_id = current_setting('app.tenant_id', true)::uuid OR (is_shareable = true AND current_setting('app.enrichment_scope', true) IN ('basic', 'full')) )FORCE ROW LEVEL SECURITYpe toate tabelele RLS (fallback deny)
7. Auth flow (Authentik OIDC)
JWT custom claims
{
"sub": "user-uuid",
"email": "...",
"tenant_id": "tenant-uuid",
"is_beletage_group": true,
"roles": ["topograf", "firm_admin"],
"enrichment_scope": "full",
"allowed_sirutas": ["54975", "155243"]
}
User attributes sync
- Webhook HMAC-signed din planhub/eterra.live la user create/update
- Idempotency key dedupe 24h
- Exponential backoff retry (6 tentative: 1,2,4,8,16,32s)
- Dead Letter Queue în pg-boss
auth-sync-dlq
Super-tenant check
Dynamic DB query la fiecare JWT emit (Authentik custom expression), cache 60s.
JWT expiry
- Access: 1h
- Refresh: 30d
8. Sync orchestrator + multi-account ANCPI
Componente
- Queue: pg-boss (schema
queue), max 8 workers, retention 30d - Workers:
eterra-sync-worker(delta/full UAT sync)enrichment-worker(CF fetch + GDPR pool split)pug-ingest-worker(GPKG upload → gis_urban drafts)pmtiles-rebuild-worker(tippecanoe → MinIO)cache-invalidate-worker(Cloudflare purge API)
Multi-account ANCPI shuffle
gis_meta.eterra_accounts (
id, username, password_encrypted (AES-256-GCM, key GIS_SYNC_AES_KEY),
account_type, quota_per_hour DEFAULT 500,
usage_current_hour, last_reset_at,
status, notes
)
Round-robin pe conturi active cu quota disponibilă. Failover auto dacă blocked. Distribution load ANCPI = zero detection singular-account patterns.
Session pool persistent (TTL 20 min)
Înlocuiește in-memory din eterra-live + hardcoded din ArchiTools.
LISTEN/NOTIFY events cross-app
sync:uat:done→ apps refresh UIpmtiles:rebuild:done→ cache invalidateenrichment:new→ user UI notify
Scheduler (pg-boss cron)
- Delta sync: Lu-Vi 2:00
- Deep sync: Weekend 23:00
- PMTiles rebuild: Lu-Vi 4:00
- Cleanup sessions: 6h
ArchiTools/eterra-live post-migration
- ArchiTools: DELETE eterra-client.ts + setInterval. Read-only consumer + trigger sync via API.
- eterra-live: Păstrează UI eterra login (per-user), trigger sync prin orchestrator API.
- Orchestrator = UNICUL scriitor gis_core.
9. Tile serving (4 subdomenii)
tiles.gis.ac (PUBLIC)
Martin 1 instanță, zero auth, CF rate limit 100 req/s/IP.
Layer groups:
/cadastru,/uat,/pug-public,/zone-protectie,/drumuri,/retele-publice/puz-aprobate,/pud-aprobate,/analize-publice,/puz-in-avizare
tiles-private.gis.ac (AUTH)
Martin-private container + Traefik ForwardAuth JWT validation. Zero cache.
Layer groups:
/pug-drafts/{tenant},/puz-drafts/{tenant},/pud-drafts/{tenant}/observatii-primarie/{siruta},/analize-interne/{tenant}
raster.gis.ac + dem.gis.ac
TiTiler (NU Martin — Martin only vector).
raster.gis.acserves COG din MinIO bucketdem.gis.ac= proxy cu cache CF la ANCPI MNT (geoportal.ancpi.ro/maps/rest/services/ANCPI/MNT)- Hillshade + contour generate local din DEM
pmtiles.gis.ac
nginx static serve din MinIO bucket pmtiles. CF cache 7 zile.
CF TTL config
| Path | TTL |
|---|---|
| tiles.gis.ac/uat | 30 zile |
| tiles.gis.ac/cadastru | 6h + auto-purge la sync |
| tiles.gis.ac/pug-public | 24h |
| tiles.gis.ac/puz-in-avizare | 1h |
| tiles.gis.ac/puz-aprobate, pud | 24h |
| pmtiles.gis.ac | 7 zile |
| tiles-private.gis.ac | zero |
| raster.gis.ac, dem.gis.ac | 30 zile |
Bot Fight Mode: Medium default.
PUZ lifecycle visibility
| Stadiu | Vizibil | Cache |
|---|---|---|
| Draft intern | tiles-private (tenant only) | zero |
| Avizare publică | tiles.gis.ac/puz-in-avizare | 1h |
| Aprobat | tiles.gis.ac/puz-aprobate (overlay pug-public) | 24h |
Raster Library module (admin eterra.live)
- Upload TIFF/GeoTIFF → background gdal_translate → COG + overviews
- Preview thumbnail 512px
- Public/private toggle
- Delete/replace
- DB:
gis_meta.raster_sources
10. API layer (api.gis.ac)
Stack
Next.js 16 + Hono router intern + Prisma + Zod + Authentik JWT middleware.
Endpoint exemple
POST /enrichment/parcela auth: tier≥basic
GET /parcela/{id} auth: optional
POST /pug/zone-functionala auth: admin_primaria
POST /sync/uat auth: admin Beletage
POST /raster/upload auth: architect cu scope
GET /search?q=... auth: optional
Rate limiting (Redis per JWT sub)
| Tier | Req/oră | Enrichments/zi |
|---|---|---|
| Free | 100 | 10 |
| Basic | 1000 | 100 |
| Pro | 5000 | unlimited |
| Admin | nelimitat | nelimitat |
Managed prin admin eterra.live.
11. MDLPA compliance (Ordin 904/2023)
Abordare Golden: schema internă flexibilă + export layer separat conform MDLPA.
Internal schema gis_urban.plan
Include ALL MDLPA required fields + extras (history, drafts, workflows, comments).
PG18 temporal constraint
ALTER TABLE gis_urban.plan_spatial ADD CONSTRAINT no_overlap_plan
EXCLUDE USING gist (siruta WITH =, doc_type WITH =,
daterange(data_aprob, data_exp, '[)') WITH &&);
Garantează zero PUG-uri suprapuse pe aceeași UAT.
Export worker
pug-export-worker(pg-boss job)- Input:
{plan_id} - Output: ZIP cu 5 subdirectoare + GPKG MDLPA-compliant + PDF-uri
- Tooling: ogr2ogr (GDAL) pentru GPKG, custom orchestrator Node
- Drop Z/M dimensions la export
- Default HILUCS_N1 dacă missing intern
- ZIP naming:
^[A-Z][A-Z]_.*?_\d{1,10}_(PUG|PUZ|PATJ|PMUD)_[0-9]{8}
Validator MDLPA
Faza 1: manual pre-submit by user (download Validator_2.0.5, run local). Faza 2: Marius primește repo validator de la friends MDLPA → containerizăm. Faza 3: custom pre-validation 80% reguli (blocker-i obvious) înaintea ZIP download.
Doc types în scope
PUG (prio 1), PUZ (prio 1), PUD (prio 2), PMUD (da), PATJ (opțional, max 1 județ).
12. Backup strategy (multi-tier)
Tier 1 — NAS local (LAN fast)
- Path:
gis-backup@10.10.10.10:gisbkp/(symlink →/HDD/gisbkp) - btrfs + COW + compression
- Retention: 30 daily + 12 monthly + 5 yearly snapshots
- Protocol: rsync over SSH, RSYNC-ONLY key flag
Tier 2 — Backblaze B2 (offsite hot)
- Bucket:
beletage-gis-backups(region EU) - Object Lock Compliance mode 90d + SSE-B2
- Weekly full + monthly archive encrypted AES-256 client-side
Tier 3 — Azure (deferred, credit €2400/year)
- Azure PostgreSQL Flexible Server (DR cloud fallback, ~50-80€/lună)
- Azure CDN + Blob Hot (PMTiles secondary, ~20€/lună)
- When: scale 10k+ users sau geo-redundanță
Tier 4 — CJ2 standby (deferred ~oct 2026)
- Streaming replication async, RPO~0, RTO<5min
- Aștept workstation AI nou + disk dedicat
pgBackRest setup
- 2 repos: NAS + B2
- PITR window: 30 zile
- Continuous WAL archive
- Compress zstd + encrypt AES-256
- Parallel restore workers
DR drill
- Trimestrial manual full recovery simulation
- Săptămânal smoke test (read-only integrity check)
- Lunar test restore automation (ephemeral container, 30 min, 20GB temp)
Backup Dashboard
Modul în admin.eterra.live, tab "Backup & Recovery":
- Cards pentru fiecare tier (NAS, B2, Azure placeholder, CJ2 placeholder)
- Plugin architecture
BackupTargetinterface — extensibil - Slot-uri viitoare: email archival, Gitea repos, workstations, configs
- Alerting Golden low-noise (email doar la red)
13. Monitoring (Dashboard admin eterra.live)
6 cards simple (RO tooltip clar)
- Hărți (req/s)
- Baza de date (query latency)
- Spațiu disc (%)
- Topografi activi
- Sincronizare (ultim)
- Backup (ultim)
Culori: verde/galben/roșu
Alert doar la roșu (zero spam)
Grafana full = collapsed "Detalii avansate"
Prometheus metrics scraped
- Martin (/metrics), Postgres (pg_stat_statements + exporter), pgBouncer, MinIO, gis-api, orchestrator
14. Securitate — posture final
Authentication
- Authentik OIDC pentru toate apps
- JWT 1h access + 30d refresh
- JWKS cache 1h
Authorization
- RLS multi-tenant (siruta + tenant_id + is_beletage_group)
- Session variables via SET LOCAL (zero leak)
- Zero Postgres role cu BYPASSRLS permanent
Secrets management
- Infisical single source of truth
- Rotate schedule: annual pentru keys critice, automatic pentru JWT
- Encryption keys: Infisical + paper print sealed Beletage safe (YubiKey viitor)
Network
- PG 5433 bind 127.0.0.1 (intern shop)
- pgBouncer 6432 bind 127.0.0.1
- Martin/TiTiler/gis-api prin Traefik only
- Sophos DNAT doar 80/443 către proxy
Audit
gis_meta.auditENCRYPTED cu pgcrypto (key AUDIT_ENCRYPTION_KEY)- Retention: 1 an hot PG + 5 ani archive MinIO
- Decryption doar la query admin explicit
Data at rest
- Backups encrypted (pgBackRest + client-side AES pentru B2)
- ENCRYPTION_SECRET rotate pending (re-encrypt 15 vault entries)
15. Ce DISPARE post-cutover
setIntervalcron-in-cod din ArchiToolseterra-client.tsdin ArchiTools (mut în orchestrator)- In-memory session pool eterra-live (persistent DB)
- PG 5432 exposed 0.0.0.0 satra (shop bind LAN)
- PMTiles webhook broken (înlocuit de pmtiles-rebuild-worker)
- systemd pmtiles-webhook parole în args (container nou)
- Hardcoded ETERRA_USERNAME/PASSWORD ArchiTools (shared pool)
- AES IV=16 eterra-live (upgrade 12)
- Rate limit in-memory eterra-live (Redis)
16. Migration timeline (6 săptămâni)
Sprint 1 — Foundation stack (săpt 1)
Day 1 (GO trigger):
- Deploy
martin-publiccontainer pe shop - Deploy
martin-privatecontainer + Traefik ForwardAuth middleware - Deploy
titilercontainer - Deploy
nginx-pmtilescontainer - Deploy
minio-giscontainer + bucket setup
Day 2:
- Deploy
pgadmin(intern pgadmin.beletage.ro) - Traefik routes pentru toate subdomeniile GIS
Day 3-4:
- Cloudflare zones: add
gis.ac,eterra.live,pug.digital,planhub.ro - DNS records + Page Rules TTL + Rate Limits + Bot Fight Medium
- Test tile serving basic
Day 5:
- Schema split pe shop:
CREATE SCHEMA gis_core, gis_urban, gis_enrichment, gis_meta, tenants, eterra, pug, archi, planhub, queueALTER TABLE public.X SET SCHEMA gis_corepentru tabele existente- Create views + roles
Sprint 2 — Auth + data layer (săpt 2)
Day 1-2:
- Authentik custom property mappings (tenant_id, is_beletage_group, enrichment_scope, allowed_sirutas)
- Webhook signed HMAC din eterra.live → Authentik sync
- PG roluri aplicație + grants
Day 3-4:
- RLS policies per tabel (gis_urban, gis_enrichment)
- FORCE ROW LEVEL SECURITY pe toate
- gis-api boilerplate (Next.js 16 + Hono + Prisma + Zod)
- Auth middleware JWT verify + SET LOCAL
Day 5:
- Cypress test suite RLS (6 scenarii obligatorii)
Sprint 3 — Sync orchestrator (săpt 3)
Day 1-2:
gis-sync-orchestratorcontainer- pg-boss setup schema queue
- eTerra client extract în shared lib
Day 3:
- Session pool persistent
gis_meta.eterra_sessions+ multi-accountgis_meta.eterra_accounts - Workers: eterra-sync, enrichment, pug-ingest
Day 4:
- Workers: pmtiles-rebuild, cache-invalidate (CF API)
- LISTEN/NOTIFY events implementation
Day 5:
- Admin UI module eterra.live — sync jobs control panel
Sprint 4 — Backup + Dashboard (săpt 4)
Day 1-2:
- pgBackRest deploy shop
- 2 repos: NAS (rsync push via cron) + B2 (direct S3)
- Scheduled jobs: daily incremental, weekly full, monthly archive
- WAL archive continuous
Day 3:
- Backup Dashboard UI (admin eterra.live tab "Backup & Recovery")
- Plugin architecture
BackupTargetinterface - Cards: NAS, B2, Azure placeholder, CJ2 placeholder
Day 4:
- DR runbook scripts
- Monthly restore test automation (ephemeral container cron)
- Prometheus + Grafana + Alertmanager deploy
Day 5:
- Grafana dashboards (GIS Overview, Martin Perf, PG Health, API Rate Limits, Raster Library)
- 6 simple cards admin eterra.live
- Alertmanager → n8n webhook (email + Telegram on red)
Sprint 5 — Test & parity (săpt 5)
Day 1-2:
- Shadow sync: orchestrator rulează în paralel cu satra setInterval (both read from ANCPI)
- Compare row counts ambele, checksum-uri, latency
Day 3:
- Load test: 1000 concurrent API requests, 500 QPS tile server
- Performance comparisons satra vs shop end-to-end
Day 4:
- Security audit final: TLS scan, dependency audit, pentest basic
- CORS, CSP, rate limits verify
Day 5:
- GO/NO-GO decision meeting
- Final checklist review
Weekend cutover (if GO)
Friday 22:00:
- Notify users (eterra.live + ArchiTools) maintenance 4h
- Traefik maintenance page
- Stop ArchiTools + eterra-live containers
- pg_dump satra final snapshot → shop (delta apply)
- Diff check paritate
- Switch DATABASE_URL containere → shop
- Deploy ArchiTools nou (cu eterra-client DELETED)
- Cloudflare DNS update: tiles.gis.ac → shop Martin
- Start orchestrator cron jobs
- Smoke test full flow
Saturday 06:00:
- Monitor dashboard
- On-call standby
Saturday 20:00:
- All green → GO production
- satra architools_postgres READ-ONLY (fallback 30 zile safety)
Sunday:
- Observation + bug bash
Cleanup (săpt 6)
- git filter-repo ArchiTools (rescrie istoria, notify re-clone)
- Structural fix compose (env_file + stack.env in .gitignore)
- Remove satra containers (după 30 zile)
- ENCRYPTION_SECRET rotate + re-encrypt 15 vault entries
- Documentation final în busc-infra
17. Runbooks (DR scenarios)
R1 — Corruption parcela X la 14:30
pgbackrest --stanza=gis --type=time --target="2026-04-20 14:29:00" restore
# Verify integrity + promote
R2 — Crash shop total → failover CJ2 (când activ)
- Promote CJ2 standby to primary
- Update DNS: tiles.gis.ac, api.gis.ac → CJ2 IP
- Restart apps pointing CJ2
- RTO target: <5 min
R3 — Ransomware
- B2 Object Lock Compliance = fișiere immutable
- Restore latest clean snapshot pre-ransomware
- Wipe compromised + rebuild from B2
R4 — Accidental DROP TABLE
pgbackrest restore --type=time --target="<moment înainte de DROP>"
# Replay WAL până exact înainte de comanda distructivă
R5 — Disaster regional (shop + CJ2 down)
- Restore din B2 arhive lunar
- Bootstrap new cluster pe alt host
- RTO: 4-8 ore
18. Next features post-production
Imediat după stable (month 1-2)
- MDLPA validator containerizat (după repo de la friends MDLPA)
- Geopackage export pipeline complet
- Raster Library module + upload UI
Medium term (month 3-6)
- PMUD workflow (rețele transport, piste, parcare)
- SmartCity360 / pug.digital launch (primul tenant primărie)
- planhub.ro SaaS pivot multi-tenant (primii arhitecți externi)
- CJ2 standby activation (după workstation AI nou)
Long term (month 6-12)
- PATJ suport (1 județ pilot)
- Azure activation (Azure PostgreSQL DR + Blob Hot CDN)
- Cross-region geo-redundanță dacă scale justifies
19. Risk register
| Risc | Probabilitate | Impact | Mitigare |
|---|---|---|---|
| Cutover fail | Low | High | Rollback DNS → satra, 30d safety window |
| Data loss cutover window | Very Low | High | Shadow sync săpt 5, delta final |
| Orchestrator bugs prod | Medium | Medium | Săpt 5 shadow prinde majoritatea |
| ANCPI block multi-account | Low | High | Shuffle + quota + alerting |
| B2 cloud outage | Very Low | Medium | NAS + CJ2 (când activ) alternative |
| Marius unavailable | Medium | High | Claude Code has full plan + memory |
20. Secrete pre-loaded în Infisical (verificate 2026-04-20)
Active (ArchiTools production)
- AUTHENTIK_CLIENT_ID, AUTHENTIK_CLIENT_SECRET
- NEXTAUTH_SECRET
- DB_PASS (satra Postgres user, will rotate at cutover)
- ETERRA_USERNAME, ETERRA_PASSWORD
- NOTIFICATION_CRON_SECRET
- BREVO_BELETAGE_API_KEY
Shop stack (pre-loaded pentru sprint 1)
- POSTGRES_GIS_SUPERUSER_PASSWORD
- POSTGRES_GIS_ARCHITOOLS_PASS
- POSTGRES_GIS_ETERRA_PASS
- POSTGRES_GIS_MARTIN_PASS
- POSTGRES_GIS_SYNC_PASS
- POSTGRES_GIS_PUG_PASS
- POSTGRES_GIS_PLANHUB_PASS
- MINIO_GIS_ROOT_USER
- MINIO_GIS_ROOT_PASSWORD
- PGADMIN_DEFAULT_PASSWORD
Security/Crypto
- GIS_SYNC_AES_KEY
- AUTHENTIK_WEBHOOK_HMAC_SECRET
- AUDIT_ENCRYPTION_KEY
- BACKUP_DASHBOARD_ADMIN_TOKEN
Backup
- BACKUP_ARCHIVE_KEY (AES-256 B2 client-side)
- PGBACKREST_REPO1_CIPHER_PASS
- STANDBY_REPLICATION_PASS (pentru CJ2 viitor)
- B2_APPLICATION_KEY_ID
- B2_APPLICATION_KEY
- B2_BUCKET_NAME (beletage-gis-backups)
- B2_ENDPOINT (s3.eu-central-003.backblazeb2.com)
De generat la moment (viitor)
- POSTGRES_GIS_TITILER_PASS (day 1 sprint 1)
- POSTGRES_GIS_APP_RW_PASS (day 1 sprint 2)
- GRAFANA_ADMIN_PASSWORD (sprint 4)
- PROMETHEUS_BASIC_AUTH (sprint 4)
- AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_SAS (când activăm)
21. Hardware/infrastructure check-list pre-GO
- ✅ Shop 10.10.10.84 reachable SSH (user dnz, groups docker+wheel)
- ✅ Shop docker 29.2.1 instalat
- ✅ Shop disk 3.3TB free
- ✅ Shop RAM 225GB avail
- ✅ Shop ports 5433, 6432 libere (POC ocupate)
- ✅ Satra SSH (user bulibasa) reachable
- ✅ Proxy Traefik v3.6.8 (10.10.10.199)
- ✅ Sophos 80/443 only WAN (LAN internal safe)
- ✅ NAS 10.10.10.10 rsync key deployed, symlink OK
- ✅ Backblaze B2 bucket + scoped key tested
- ✅ Authentik auth.beletage.ro v2025.2.4 operational
- ✅ Infisical toate secretele pre-loaded
- ✅ Cloudflare API token disponibil
22. Acceptance criteria (Definition of Done)
Production considerat stable când:
- Toate 13 containere shop running + healthy
- Cypress RLS suite 100% pass
- Load test 500 QPS tiles + 100 QPS api sub 200ms p95
- Backup test restore successful (monthly)
- ArchiTools + eterra.live live pe shop DATABASE_URL
- Orchestrator UNICUL scriitor gis_core
- Zero error logs 72h continuous
- Dashboard admin eterra.live arată toate verde
- DR runbook validat (scenario R1 + R4 testat)
- Grafana dashboards populated
- Users notificați + feedback pozitiv
23. Contacts & ownership
- Owner: Marius Tarau (m.tarau@beletage.ro)
- Pair: Claude Code (via Anthropic Opus 4.7 1M context)
- Escalation: m.tarau@beletage.ro + (telefon Beletage)
- On-call during cutover weekend: Marius primary, Claude assist
24. Changelog
- v1 DRAFT (2026-04-18) — initial propunere
- v2 FINAL (2026-04-20) — approved, includes all decisions from Pas 1-7, POC results, security rotation status, multi-account ANCPI shuffle, super-tenant Beletage group, Azure credit alloc, NAS + B2 tested, Backup Dashboard plugin architecture
END OF PLAN. Next action: when Marius sends "go" → Sprint 1 Day 1 begins.