- CLAUDE.md: TZ, SSO, vault-steward, versioning, persona v2.0, multitenant, KB RAG - docs/standards: persona-conversational-rules v2.0 - docs/STANDARD_*: installer-integration, email-relay, AI-prodotto, marketing-tenant, multitenant - AGENT_CHANGES.md + OPEN_TICKETS.md (registri agent automatico) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
16 KiB
Standard marketing-tenant-provisioning v1.4
Owner: AgileHub MARKETER agent (governance) + TITAN (esecuzione DKIM) + MAESTRO (orchestratore atomico AC30)
Stato: adopted (LIVE 2026-04-26 mattina v1.3 — DKIM per-tenant; v1.4 mattina — AC30 pronto in branch, deploy pending trigger)
Applies to: * (tutti i prodotti suite — TRPG, SUSTAINAI, NIS2, LG231, TAXAI, DFM, MKTG, ALLRISK, WMS, MADEBYCLOUD, AGILEHUB)
Versione: 1.4
MS implementatore: nexus-marketing-ms (porta 4221) + email-automation-ms (porta 4004) + OpenDKIM milter Postfix Helsinki + agilehub-workflow-engine (porta 4230, blocco AWE AC30_MarketingTenantProvision)
Endpoint base: /api/marketing/*
1. Scopo
Lo standard definisce il contratto cross-suite tra prodotti consumer e modulo Marketing AgileHub centralizzato. Ogni prodotto (TRPG come pilot) consuma 28 endpoint stabili /api/marketing/* per fornire ai propri clienti (consulting firm) un servizio email marketing white-label multi-tenant.
2. Multi-tenancy
| Concetto | Identificativo | Source of truth |
|---|---|---|
| Tenant marketing | tenant_id (es. tnt_agile-technology_b4f9a3141333) |
nexus_marketing_db.tenants |
| Consulting firm consumer | consulting_firm_id (FK logico) |
DB del prodotto consumer (es. trpg.consulting_firms) |
firm_slug |
derivato da slugify(consulting_firm.name) |
calcolato server-side al provisioning |
| API key per-tenant | ah_live_* (prod) / ah_test_* (sandbox) |
nexus_marketing_db.api_keys |
Vincolo: 1:1 tra tenant_id e consulting_firm_id nel prodotto consumer (nessuna 1:N).
Naming convention firm_slug (formalizzato v1.1):
- Algoritmo:
slugify(consulting_firm.name)— lowercase ASCII, separatore-, rimozione caratteri non alfanumerici, max 64 char - Esempi:
"Tremolada Consulting S.r.l."→tremolada-consulting"Agile Technology s.r.l."→agile-technology"O'Brien & Co."→obrien-co
tenant_idauto-generato server-side:tnt_${firm_slug}_${randomId(6)}(esempio:tnt_agile-technology_b4f9a3141333)- Sottodominio derivato:
{firm_slug}.agile.software(es.agile-technology.agile.software) - Reply-to default:
noreply@{firm_slug}.agile.software - Il consumer NON costruisce mai il
firm_slugautonomamente: lo riceve in risposta aPOST /admin/tenantse lo persiste come dato derivato
3. Autenticazione
- Header obbligatorio:
X-AgileHub-Key: <api_key_plaintext> - Header obbligatorio:
X-AgileHub-API-Version: 1.0 - Hash SHA-256 della key in DB (
api_keys.key_hash); plaintext mai persistito leggibile (cifrato AES-256-GCM inapi_keys.key_encrypted) - Provisioning admin via endpoint interno
/api/marketing/admin/tenants/:id/api-keys(headerX-Internal-Keysolo Esperto Agile/installer)
4. API versioning policy
- Header request:
X-AgileHub-API-Version: 1.0(obbligatorio) - Header response:
X-AgileHub-API-Version: 1.0(echo) - Header response:
X-Request-Id: req_<uuid>(cross-system tracing) - Deprecation policy: 6 mesi notice (
X-Deprecation-Date) + 6 mesi sunset (X-Sunset-Date) prima rimozione versione precedente - Canary opt-in:
X-API-Canary: truesu feature flag tenant
5. Rate limiting
| Endpoint family | Default | Override |
|---|---|---|
| Read endpoints | 60 req/min | – |
/contacts/import |
10 req/min | RATE_LIMIT_IMPORT env |
/segments/preview |
20 req/min | RATE_LIMIT_PREVIEW env |
| Tracking pixel/click | nessuno (pubblico) | – |
Header response: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.
Risposta 429 con retry_after_sec.
6. Error envelope
{
"error": {
"code": "STRING_CODE",
"message": "Human readable",
"details": { ... opzionale ... }
},
"request_id": "req_<uuid>"
}
Codici stabili documentati: MISSING_API_KEY, INVALID_API_KEY, API_KEY_REVOKED, API_KEY_EXPIRED, TENANT_NOT_FOUND, TENANT_DELETED, TENANT_SUSPENDED, UNSUPPORTED_API_VERSION, RATE_LIMITED, DSL_INVALID, DSL_TOO_DEEP, DSL_FIELD_NOT_ALLOWED, DSL_OP_NOT_ALLOWED, CONTACT_SUPPRESSED, CONTACT_NOT_FOUND, INVALID_EMAIL, CAMPAIGN_NOT_FOUND, CAMPAIGN_LOCKED, CAMPAIGN_FINAL, CAMPAIGN_NOT_SCHEDULABLE, NO_RECIPIENTS, TEMPLATE_NOT_FOUND, TEMPLATE_INVALID, TEMPLATE_REQUIRED, EXPORT_JOB_NOT_FOUND, IMPORT_JOB_NOT_FOUND, INTERNAL_ERROR.
7. SLA
| Indicatore | Target | Note |
|---|---|---|
| Uptime | 99.5% rolling 30gg | Allineato altri MS suite |
| p95 GET | <500ms | Cache Redis 30s su preview |
| p95 POST | <2000ms | DB persist + audit + idempotency |
/segments/preview 50k contatti |
<2s | Cache 30s + indici DB |
| RTO | 30 min | mysqldump nightly + restore documentato |
| RPO | 1h | Replication MySQL slave (futuro) |
8. GDPR compliance
- Double opt-in obbligatorio (POST
/contactsconconsent_pre_confirmed=falseinvia email confirm) - Footer unsubscribe RFC 8058 auto in tutti i template (one-click
List-Unsubscribe+List-Unsubscribe-Post) - Suppression list permanente per-tenant (
unsubscribe,complaint,hard_bounce,gdpr_request) - Hard delete contact (
POST /contacts/:id/hard-delete) ritornaaudit_trail_id - Export ZIP portabilità (
POST /exports) — contacts.csv + campaigns.json + suppression.csv - Offboarding tenant: 90gg grace → hard delete (zero retention post-offboard)
- Retention metrics_events: 90gg rolling, cron pulizia notturna
9. Branding tenant
- Sottodominio
{firm-slug}.agile.softwareper envelope From - 4 placeholder firm:
firm.name,firm.logo_url,firm.primary_color,firm.accent_color - 3 personalization tag contact:
contact.first_name,contact.last_name,contact.email - Reply-to configurabile per tenant
- DKIM selector dedicato per tenant (deliverability isolation)
10. 28 endpoint
Vedi /var/www/agile-services/nexus-marketing-ms/src/routes/* o ticket TICKET_AGILEHUB_MARKETING_API.md per spec completa. Riassunto:
- Sprint 2 (5 read-only):
/tenants/me,/contactsGET,/campaignsGET/{id},/templatesGET,/metrics/overview - Sprint 3 (12 write): contacts POST, segments POST/GET/preview, campaigns POST/PATCH/schedule/cancel, templates render, test-send, metrics per-campaign
- Sprint 4 (10 GDPR): contacts PATCH/DELETE/hard-delete/import, exports POST/{id}, quota, admin tenants suspend/resume/delete
11. Sandbox
- URL primary:
https://agilehub-staging.agile.software/api/marketing(LIVE 2026-04-25) - URL secondary (legacy):
https://staging.agilehub.agile.software/api/marketing - Tenant test corrente:
tnt_agile-technology_b4f9a3141333(plan='enterprise_test',quota_default=5000) - Tenant precedente:
tnt_test_tremolada_001(suspended 2026-04-25 sera, swap-out per testing su entità Agile Technology s.r.l.) - Key dedicata:
ah_test_*(mai mescolare conah_live_*) - DB:
nexus_marketing_db(clone notturno cron) - Subdomain tenant:
{firm_slug}.agile.software(es.agile-technology.agile.software) — zone Cloudflare-managed, automazione full - DNS deliverability sandbox: SPF + DMARC + DKIM tutti
active(LIVE 2026-04-26 mattina) - DKIM signing: OpenDKIM milter su Postfix Helsinki, selector
agile2026, RSA 2048, rotation annuale Q1
12. Deroghe
I prodotti consumer non possono opporsi unilateralmente. Per esenzioni: aprire ticket AgileHub con tag standard-exception + motivazione tecnica + termine. MARKETER + REGENT decidono entro 5gg lavorativi.
13. Distribuzione
| Prodotto | docs_file | claude_md_section | claude_memory | adoption_status |
|---|---|---|---|---|
| trpg | ✅ TODO | ✅ TODO | ✅ TODO | pending → acknowledged after deploy |
| sustainai | TBD M5 | – | – | pending |
| nis2 | TBD M5 | – | – | pending |
| altri 8 | TBD post-Tremolada | – | – | pending |
14. Riferimenti
- Spec produttore TRPG:
/var/www/trpg-agile/docs/PROMPT_MARKETING_FEATURE.mdv1.3 - Spec contratto:
/var/www/trpg-agile/docs/TICKET_AGILEHUB_MARKETING_API.md - Doc agente MARKETER:
docs/AGENT_MARKETER.md - Reply formale TRPG:
docs/REPLY_TO_TRPG_MARKETING_REQUEST_DRAFT.md - Audit gap:
docs/HANDOVER_FROM_TRPG_MARKETING_FEATURE_REQUEST.md
15. Changelog
v1.4 (2026-04-26 mattina, AC30 blocco AWE pronto in branch — MAESTRO impl + 11/11 test)
- Nuovo blocco AWE
AC30_MarketingTenantProvisionimplementato + testato (619 righe blocco + 397 righe test, file untracked nel branchfeature/awe-m1-m2-foundation) - Orchestrazione atomica end-to-end provisioning tenant marketing in 1 nodo workflow: tenant create + 3 DNS Cloudflare (A + SPF + DMARC) + DKIM SSH exec
dkim-provision.js+ DPA opzionale + API key generation - Compensation pattern: hook
compensate(ctx, result)engine-level + rollback inline su partial-state failure (cancella DNS records, revoca DKIM via--revoke, soft-delete tenant 90gg grace) - Idempotency H7:
sha256(runId + nodeId + {firmSlug, firmLegalName})TTL 7gg - Dry-run mode completo (mock conforme
outputSchema, no side-effect) - Vault integration:
tier1__nexus-hub-ms__cloudflare/api_tokenper CF token,INTERNAL_SERVICE_KEYenv per X-Internal-Key marketing-ms admin endpoints - SSH credential: pattern
loadAndDecrypt(sshCredentialId)riusato da AC23 - Auto-loaded dal catalog registry (
src/blocks/index.js) come 13° bloccoaction - Test verdi: 11/11 (6 scenari §9 handover MAESTRO + 3 schema/shape + 2 dryRun) — happy path + rollback DNS + rollback DKIM + skipDkim + firm_slug duplicato + idempotency
- Trigger originale Q3 2026 (≥3 tenant prod), implementazione anticipata disponibile on-demand
- Decisione GO/NO-GO deploy: pending — richiede
pm2 reload agilehub-workflow-engine+ bump health.js block count + smoke E2E contro marketing-ms reale - Spec autoritativa:
HANDOVER_AC30_MARKETING_TENANT_PROVISION_FOR_MAESTRO.md - Coordinamento agenti (al deploy): MAESTRO (impl) + TITAN (SSH exec coord) + MARKETER (acceptance + bump v1.5 con esempio workflow JSON pubblicato) + PRISMA (form auto-render verify) + VIGILE (security inventory blocco side-effect)
v1.3 (2026-04-26 mattina, DKIM per-tenant LIVE — TITAN Phase 1-4)
- DKIM signing OPERATIVO: OpenDKIM 2.11.0 installato su Postfix Helsinki, milter
inet:localhost:8891,milter_default_action=accept(fail-open per zero downtime) - Schema scope chiave: 1 keypair RSA 2048 per-tenant, selector
agile2026._domainkey.{firm_slug}.agile.software. Pattern conferma standard §16 (deliverability isolation per-tenant). - Tenant Agile Technology: keypair generata + DNS TXT pubblicato Cloudflare + KeyTable + SigningTable + DB UPDATE →
dkim_status='active' - Automation script:
scripts/dkim-provision.jsself-contained Node.js (no npm install, mysql CLI + https native + execSync). Idempotente. Eseguito on-host Hetzner. - §16 ggiornato:
Strategia DKIM sandbox vs prodrimossa "skip sandbox" (DKIM ora attivo anche in sandbox per testing realistico). Mantengonodkim_status='not_required'come stato simbolico per tenant esplicitamente esentati. - Rotation policy: annuale Q1 (Q1 2027 prossimo turno), dual-key 30gg overlap durante transizione, coordinata con VIGILE Pillar 1
- Backward compat: 99.994% del traffico Postfix Helsinki (cron, www-data, noreply@agile.software) NON matcha SigningTable → continua identica a oggi (15.466 email/7gg invariate, verificato empiricamente)
v1.2 (2026-04-25 sera, post-allineamento Cristiano TRPG — stesso giorno di v1.1)
- Convention dominio cambiata:
*.agilehub.it→*.agile.software(correzione architetturale: Agile ha un solo dominio commercialeagile.software, già zone Cloudflare-managed per altri sottodomini cometrpg.agile.software,agilehub.agile.software) - Schema
tenant_branding.dkim_status: ALTER additive enum, aggiunto valorenot_requiredper sandbox/non-prod (oltre apending/active/failed) - Tenant
tnt_agile-technology_b4f9a3141333: UPDATE branding subdomain →agile-technology.agile.software, reply_to →noreply@agile-technology.agile.software, dkim_status=not_required, spf_status=active, dmarc_status=active - §16 riscritto: sostituito vecchio scenario register.it con scenario reale Cloudflare-managed
- §11 Sandbox: aggiornato per nuovo subdomain pattern + DNS deliverability LIVE
- DKIM strategia: skipped per sandbox, mandatory per produzione (go-live Tremolada 23/6 — vedi §16)
v1.1 (2026-04-25 sera, superseded by v1.2 stesso giorno)
- §2 Multi-tenancy: formalizzato
firm_slug = slugify(consulting_firm.name)come algoritmo derivato server-side. Aggiunti 3 esempi concreti, regolatenant_id = tnt_${firm_slug}_${randomId(6)}, derivazione automatica sottodominio + reply-to. - §11 Sandbox: aggiornato tenant corrente a
tnt_agile-technology_b4f9a3141333(sostituiscetnt_test_tremolada_001suspended) - v1.1 documentava convention
*.agilehub.it(corretta in v1.2 stesso giorno)
16. Gestione zone DNS *.agile.software + deliverability
Provider DNS authoritative: Cloudflare (zone id e3d677355677a6397d2caa77264cbfa2).
MX inbound: separato (Microsoft 365 su zone agilehub.it); modulo Marketing fa SOLO outbound, no impatto MX.
Automazione: full via Cloudflare API (token in vault tier1__nexus-hub-ms__cloudflare/api_token).
Pattern record per tenant (provisioning automatico con MARKETER+TITAN):
| Record | Esempio Agile Technology | Scope |
|---|---|---|
A |
agile-technology.agile.software → 135.181.149.254 |
Helsinki Postfix outbound |
TXT SPF |
v=spf1 ip4:135.181.149.254 -all |
autorizzazione mittente |
TXT DMARC |
_dmarc.agile-technology.agile.software TXT v=DMARC1; p=none; rua=mailto:dmarc-reports@agile.software; pct=100 |
report-only modalità monitoring |
TXT DKIM |
agile2026._domainkey.agile-technology.agile.software TXT v=DKIM1; k=rsa; p=<pubkey> |
solo PROD, skip sandbox |
Strategia DKIM sandbox vs prod (v1.3 LIVE 2026-04-26):
- DKIM mandatory per tutti i tenant produttivi (no skip). DMARC inizia
p=none(monitoring) e bumppap=quarantinepoip=rejectquando deliverability stabile post-3 settimane di metriche pulite. dkim_status='not_required': stato simbolico per tenant esplicitamente esentati (es. tenant disabilitati o test temporanei senza traffico real-world). Widget consumer lo mappa come "n/a".- Sandbox attiva DKIM by default dal v1.3:
agile-technology.agile.softwareha DKIMactiveper testing realistico pre-prod.
Effort provisioning per nuovo tenant prod (con script automation v1.3):
- POST
/admin/tenants(auto-genera firm_slug + subdomain) — 1s - POST 3 record Cloudflare base (A + SPF + DMARC) — manuale via curl o futuro workflow AWE — 10s
node scripts/dkim-provision.js --tenant-id <id> --firm-slug <slug>ON HOST HETZNER (root):- genera keypair RSA 2048 (idempotente)
- append KeyTable + SigningTable
- publish DNS DKIM TXT su Cloudflare (idempotente, PATCH se esiste)
- UPDATE
tenant_branding.dkim_*+dkim_status='active' systemctl reload opendkimhot- ~3-5 secondi end-to-end
Totale: ~15s tenant pronto. Future workflow AWE blocco AC30_MarketingTenantProvision Q3 2026 può chiamare script via webhook host.
Rotation policy DKIM (annuale Q1, coord VIGILE):
- Q1 ogni anno: VIGILE notifica scadenza
- TITAN: nuovo selector (es.
agile2027per Q1 2027), genera keypair nuovo - Pubblica DNS DKIM nuovo (entrambi vecchio+nuovo coesistono per 30gg overlap)
- Aggiorna
KeyTable+SigningTableper usare nuovo selector systemctl reload opendkim→ email firmate con nuova chiave- T+30gg: revoca vecchio selector (DELETE DNS vecchio + cleanup KeyTable/SigningTable)
- Audit log TITAN+VIGILE entry coordinated
Comandi rapidi rotation (futuro Phase 4 enhancement):
# T0: nuova chiave
node scripts/dkim-provision.js --tenant-id <id> --firm-slug <slug> --selector agile2027
# T+30gg: revoca vecchia
node scripts/dkim-provision.js --revoke --tenant-id <id> --firm-slug <slug> --selector agile2026