GSSO — Technical Documentation
| Document code | SPEC-GSSO-2026 / Report 04 |
| Version | 0.1-draft (EN source, governing) |
| Date | 2026-10-05 |
| Status | Draft: describes the planned system. No code exists yet |
| Companion reports | 00 RFP · 01 ADR · 02 Requirements · 03 Consumers & contract · 05 Roadmap |
Revision history
Section titled “Revision history”| Version | Date | Changes |
|---|---|---|
| 0.1-draft | 2026-10-05 | C4 views, components, data model, flows, realm baseline, security, deployment, takeover runbook, sizing, observability, testing |
1. System context (C4 level 1)
Section titled “1. System context (C4 level 1)”flowchart TB admin([IAM admin / platform owner / approver / auditor / helpdesk]) staff([Staff user]) citizen([Citizen]) apps[gStack SaaS & PaaS<br/>interdictii, cancelaria, gdocs, gregistry, drumuri,<br/>CRM, gDocFlow, gTenders, glog, gnotify, gstorage, gflow] mob[gsso_mob GovSign] gsso[[GSSO<br/>identity & SSO platform]] mpass[MPass / eID] ldap[AD / LDAP of institutions] glog[GLog] gnotify[GNotify] smtp[SMTP relay]
admin -- console --> gsso staff -- login / SSO --> gsso citizen -- login --> gsso mob -- OIDC PKCE --> gsso apps -- OIDC, JWKS, api/v1/app, Kafka events --> gsso gsso -- SAML broker --> mpass gsso -- LDAP federation --> ldap gsso -- audit events --> glog gsso -- alerts, expiry notices --> gnotify gsso -- invitations, reset mails --> smtp2. Containers (C4 level 2)
Section titled “2. Containers (C4 level 2)”flowchart LR subgraph edge[Edge] nginx[nginx / ingress<br/>sso.gstack… · gsso.gstack…] end subgraph gssoSys[GSSO] web[gsso-web<br/>Angular + GDS<br/>static] gw[gsso-gateway<br/>JHipster 9.1 gateway · BFF] srv[gsso<br/>JHipster 9.1 microservice] db[(PostgreSQL<br/>gsso)] kc[Keycloak 26.6.x<br/>+ gsso-kc-extensions<br/>+ theme gstack] kdb[(PostgreSQL<br/>keycloak)] boot[gsso-bootstrap<br/>keycloak-config-cli job] end kafka[[Kafka]] glog[GLog] gn[GNotify]
nginx --> web nginx --> gw nginx --> kc gw -- /api/v1/** --> srv gw -. OIDC code flow, client gsso-console .-> kc srv -- Admin REST, SA gsso-reconciler --> kc srv --- db kc --- kdb boot -- baseline realms --> kc kc -- gsso.kc-events.v1 --> kafka kafka --> srv srv -- gsso.access-revoked.v1, gsso.grant.v1 --> kafka srv -- audit --> glog srv -- notify --> gn| Container | Tech | Responsibility |
|---|---|---|
gsso-web | Angular (version per JHipster 9.1.0), @gstack/gds-angular, ngx-translate ro/ru/en | Console UI; no tokens (BFF cookie) |
gsso-gateway | JHipster 9.1.0 gateway (Spring Cloud Gateway), OAuth2 client | Login and session (BFF), CSRF, routing of /api/v1/**, rate limits on app zone |
gsso | JHipster 9.1.0 microservice, Spring Boot, Hibernate, Liquibase, Kafka, keycloak-admin-client 26.x | Catalog, grants, reconciler, event consumer, relay to GLog, reports, admin and app APIs |
| Keycloak | quay.io/keycloak/keycloak:26.6.x (custom image with extensions + theme, kc.sh build) | Authentication, tokens, sessions, MFA, federation, account console |
gsso-bootstrap | adorsys/keycloak-config-cli matching 26.x | Applies realm baselines on deploy (idempotent) |
| PostgreSQL ×2 | as generated by JHipster 9.1.0 (gsso); Keycloak-supported version (keycloak) | Separate databases, backups, lifecycles |
| Kafka | shared gStack cluster (Strimzi target; single broker in compose) | Event transport |
2.1 Repository layout (govtech/gstack/gsso)
Section titled “2.1 Repository layout (govtech/gstack/gsso)”gsso/ CLAUDE.md gsso.jdl gsso.dc.html test-scenarios.md .env.example docs/reports/ 00..05 (en, ro, ru), README, docx/, pdf/, img/ keycloak/ extensions/ Maven module: gsso-kafka event listener, mappers (roles, roles_scoped, org_unit, tenant), conditional-role authenticator themes/gstack/ login/, account/, email/ (FreeMarker + GDS CSS, messages_ro/ru/en) realms/ gstack.json, cetatean.json, tenant-template.json (keycloak-config-cli, ${ENV} placeholders) Dockerfile FROM keycloak:26.6.x → kc.sh build with providers + theme gsso/ JHipster 9.1.0 microservice (generated from gsso.jdl + custom code) gsso-gateway/ JHipster 9.1.0 gateway gsso-web/ Angular SPA on GDS sdk/ gsso-spring-boot-starter/ gsso-angular/ deploy/ docker-compose.yml local + demo host stack docker-compose.test.yml postgres + kafka + keycloak for IT (always `down -v`) nginx/gsso.conf vhost templates (applied only with owner approval) helm/gsso/ Kubernetes target testing/results/3. Components of gsso
Section titled “3. Components of gsso”Package (systems.esempla.gsso) | Component | Notes |
|---|---|---|
domain, repository, service, web.rest | JHipster-generated CRUD for JDL entities | regenerated; custom code lives in separately named classes |
keycloak | KeycloakAdminGateway | Only class touching keycloak-admin-client. Natural-key lookups, gsso.managed marker, error translation to RFC 7807 |
sync | JobOutbox, ReconcilerWorker, RealmDiffer, DriftPolicy | Outbox jobs, worker with FOR UPDATE SKIP LOCKED + per-realm advisory lock, periodic full diff |
sync.handlers | RealmHandler, ClientHandler, RoleHandler, RoleMappingHandler, UserHandler, PolicyHandler, OrgUnitHandler | One handler per object type: apply(job), diff(realm) |
grant | GrantLifecycleService, GrantExpiryScheduler, ApprovalPolicy | State machine, four-eyes, expiry, notifications |
events | KcEventConsumer, EventStore, HashChain, GlogRelay, RevocationPublisher | Idempotent consumption, append-only store, forwarding, outgoing events |
projection | UserProjectionUpdater, NightlyUserSync | ProiectieUtilizator |
report | DashboardAggregator, AccessReport | KPIs, login buckets, auth mix |
web.rest.admin, web.rest.app | Facade controllers | Zones, scopes, realm scoping filter, Idempotency-Key store |
security | RealmScopeAuthorizationManager, PlatformScopeAuthorizationManager | GSSO_REALM_ADMIN/GSSO_PLATFORM_OWNER scoping |
notify | GnotifyClient | Expiry warnings, alerts |
4. Data model
Section titled “4. Data model”erDiagram REALM ||--o{ CLIENT_APLICATIE : contains REALM ||--|| POLITICA_AUTENTIFICARE : has REALM ||--o{ UNITATE_ORGANIZATIONALA : has REALM ||--o{ PROIECTIE_UTILIZATOR : projects PLATFORMA ||--o{ CLIENT_APLICATIE : owns PLATFORMA ||--o{ ROL_PLATFORMA : exposes PLATFORMA }o--o{ REALM : "deployed in" ROL_PLATFORMA ||--o{ ROL_PLATFORMA : "composite of" PACHET_ROLURI }o--o{ ROL_PLATFORMA : bundles ATRIBUIRE_ACCES }o--|| ROL_PLATFORMA : grants ATRIBUIRE_ACCES }o--o| PACHET_ROLURI : "via bundle" ATRIBUIRE_ACCES }o--o| UNITATE_ORGANIZATIONALA : "scoped to" ATRIBUIRE_ACCES ||--o{ APROBARE : "approved by" UNITATE_ORGANIZATIONALA ||--o{ UNITATE_ORGANIZATIONALA : parent JOB_SINCRONIZARE }o--|| REALM : targets EVENIMENT_GSSO }o--|| REALM : inThe authoritative definition is gsso.jdl. Main fields:
| Entity | Key fields | Notes |
|---|---|---|
Realm | nume (unique), denumireAfisata, tip {STAFF, CETATEAN, TENANT, SERVICE}, brokerAlias, politicaDrift {REPORT, ENFORCE, IGNORE}, stare {ACTIV, INACTIV}, stareSync, kcId, adoptat | |
Platforma | cod (unique), denumire, tip {SAAS, PAAS}, descriere (ro/ru/en), urlBaza, accent, stare, autoInregistrare | owners via ProprietarPlatforma(sub) |
ClientAplicatie | clientId, protocol {OIDC, SAML}, tipAcces {PUBLIC_PKCE, CONFIDENTIAL, BEARER_ONLY, SERVICE_ACCOUNT}, redirectUris, webOrigins, postLogoutUris, backchannelLogoutUrl, audienta, scopuri, audienteSchimb, sensibil, stare, stareSync, kcId | unique (realm, clientId) |
RolPlatforma | cod (prefixed, unique per realm), descriereRo/Ru/En, compozit, sensibil, valabilitateImplicitaZile, stareSync, kcId | children via self-relation |
PachetRoluri | cod, denumire, roles | reconciled as Keycloak group |
AtribuireAcces | userSub, realm, rol / pachet, unitate, validDe, validPana, stare {SOLICITATA, APROBATA, ACTIVA, RESPINSA, REVOCATA, EXPIRATA}, solicitant, motiv, sursa {CONSOLA, APP, ADOPTAT, DIRECT}, motivRevocare | |
Aprobare | atribuire, aprobator, nivel (1/2), decizie, comentariu, data | |
UnitateOrganizationala | cod, denumire (ro/ru/en), parinte, sefi | |
PoliticaAutentificare | parola, otp, webauthn, qr, client, broker, magic (booleans), politicaParola, mfaRoluriSensibile, sesiuneInactivMin, sesiuneMaxOre, tokenAccesMin | one per realm |
JobSincronizare | realm, tipObiect, cheieObiect, operatie {CREATE, UPDATE, DELETE, MAP, UNMAP, DIFF}, stare {PENDING, RUNNING, OK, FAILED, DRIFT, ACCEPTAT}, incercari, urmatoareaIncercare, eroare, diff (jsonb), corelatie | |
ProiectieUtilizator | sub, realm, username, nume, email, idnpCriptat, activ, mfa (set), ultimaAutentificare, ultimulEsec, unitate, locale, actualizatLa | read model |
EvenimentGsso | id (UUID), realm, tip, categorie {KC_USER, KC_ADMIN, CATALOG, GRANT, SYNC, CONSOLE}, actor, subiect, client, ip, moment, payload (jsonb), hashPrecedent, hash, glogId | append-only; monthly partitions |
JSONB columns (diff, payload, descriere) are declared as TextBlob in the JDL and converted by an extra Liquibase changelog with @JdbcTypeCode(SqlTypes.JSON). Changelogs are append-only.
5. End-to-end flows
Section titled “5. End-to-end flows”5.1 Register a platform → clients and roles in Keycloak
Section titled “5.1 Register a platform → clients and roles in Keycloak”sequenceDiagram actor A as Platform admin participant W as gsso-web participant G as gsso-gateway participant S as gsso participant DB as gsso DB participant R as ReconcilerWorker participant K as Keycloak A->>W: Create platform "crm" + clients + roles W->>G: POST /api/v1/admin/platforme (Idempotency-Key) G->>S: relay (user token) S->>DB: tx: Platforma, ClientAplicatie*, RolPlatforma*, JobSincronizare*, EvenimentGsso S-->>W: 201 (stareSync=PENDING) R->>DB: take jobs (SKIP LOCKED, realm lock) R->>K: GET client by clientId → absent → POST client (gsso.managed=true) R->>K: POST roles, composites, audience mapper R->>DB: job OK, kcId stored, stareSync=IN_SYNC, event SYNC_OK W->>G: poll / refresh → badge "in sync"5.2 Grant request → approval → token
Section titled “5.2 Grant request → approval → token”sequenceDiagram actor Req as Requester actor Own as Platform owner actor Ap2 as Approver (sensitive) participant S as gsso participant K as Keycloak participant N as GNotify Req->>S: POST /admin/atribuiri {sub, CRM_ADMIN, motiv} S->>S: SOLICITATA (+event) S->>N: notify owner Own->>S: approve (level 1) alt role sensitive Ap2->>S: approve (level 2, ≠ requester, ≠ owner approval) end S->>S: APROBATA + Job MAP S->>K: role-mapping add (reconciler) K-->>S: 204 S->>S: ACTIVA (+event, gsso.grant.v1) Note over K: next token of the user contains CRM_ADMIN in roles5.3 Revocation within 60 s
Section titled “5.3 Revocation within 60 s”sequenceDiagram actor O as Owner participant S as gsso participant K as Keycloak participant Kf as Kafka participant App as App (starter) O->>S: revoke grant (motiv) S->>K: role-mapping delete S->>K: POST /users/{id}/logout (sessions + back-channel logout) S->>Kf: gsso.access-revoked.v1 {sub, notBefore=now} Kf->>App: deny-list sub until token TTL App-->>App: requests with iat < notBefore → 401 S->>S: REVOCATA (+event → GLog)5.4 Login event → dashboard and GLog
Section titled “5.4 Login event → dashboard and GLog”sequenceDiagram participant U as User participant K as Keycloak (+gsso-kafka) participant Kf as Kafka participant S as gsso participant L as GLog U->>K: login (password + OTP) K-->>U: tokens K--)Kf: LOGIN event (async) Kf->>S: consume (dedup eventId) S->>S: EvenimentGsso (hash chain), projection.lastLogin, aggregates S->>L: POST /api/v1/app/audit-events (client credentials) L-->>S: receipt id5.5 Full reconcile and drift
Section titled “5.5 Full reconcile and drift”- Every 15 min, or on demand,
RealmDifferloads the desired state for the realm and reads Keycloak (clients, roles, composites, mappings of managed roles, groups, flows and policy fields it manages). - For each difference it creates a
JobSincronizare(DIFF):- missing in Keycloak →
CREATE(when the policy isENFORCE) orDRIFT; - different →
UPDATEorDRIFT; - present in Keycloak with
gsso.managed=truebut absent in GSSO →DELETEorDRIFT; - present without the marker →
NEGUVERNAT(report only).
- missing in Keycloak →
- In the console an admin can retry, enforce (apply the GSSO value) or accept drift (copy the Keycloak value into GSSO).
5.6 Adoption of an existing realm (e.g. interdictii)
Section titled “5.6 Adoption of an existing realm (e.g. interdictii)”This is a dry run first (GSSO-FR-080), then the import:
- clients are grouped into platforms by a mapping file (
clientId→ platform code); - realm roles are mapped to platform roles through rename rules (for example
ROLE_ADMIN→ per-platform<APP>_ADMINcandidates, flagged for review); - current user→role mappings become
AtribuireAcces(ACTIVA, sursa=ADOPTAT).
Nothing is written to Keycloak while the drift policy is IGNORE.
5.7 Console login (BFF)
Section titled “5.7 Console login (BFF)”- The browser opens
gsso.gstack…, andgsso-gatewayredirects it to Keycloak (realmgstack, clientgsso-console, PKCE + confidential). - The user logs in. MFA is mandatory for any
GSSO_*role (NFR-SEC-008). - The gateway stores the tokens in the server-side session and sets the
SESSIONcookie (HttpOnly,Secure,SameSite=Lax) plus the CSRF cookie. - SPA calls carry the cookie. The gateway relays the access token to
gsso, which applies the realm and platform scope from theGSSO_*roles and their attributes.
5.8 Citizen login through MPass
Section titled “5.8 Citizen login through MPass”- A
cetateanclient redirects the citizen to Keycloak, which brokers to MPass (SAML POST). - The assertion returns. The first-broker-login flow looks up the user by the IDNP attribute; it links the account if found, otherwise it creates the user with the mapped attributes.
- Keycloak sets the
acrvalue from the MPass assurance level, and the token is issued for the portal.
6. Realm baseline (keycloak/realms/gstack.json, summary)
Section titled “6. Realm baseline (keycloak/realms/gstack.json, summary)”| Setting | Value |
|---|---|
| Login theme / account / email | gstack (GDS), locales ro, ru, en, default ro |
| Brute force | on, 5 failures, wait increment 60 s, max 15 min |
| Password policy | length(12) and upperCase(1) and lowerCase(1) and digits(1) and notUsername and notEmail and passwordHistory(5) |
| SSO session idle / max | 30 min / 10 h |
| Access token | 5 min |
| Events | user + admin events enabled, admin events with representation, listeners jboss-logging, gsso-kafka, expiry 7 days |
| Client scopes (default) | profile, email, gsso-roles (flat roles), gsso-org (org_unit, org_unit_path, roles_scoped), gsso-locale, acr |
| Client scopes (optional) | idnp, gsso-roles-filtered, offline_access |
| Browser flow | Cookie (alt) → Identity provider redirector (alt) → Forms: username+password (req) → Conditional OTP/WebAuthn (condition: user has role GSSO_MFA_REQUIRED or any role flagged sensitive) |
| Default roles | ROLE_USER |
| Baseline clients | gsso-console (confidential, standard flow, PKCE), gsso-api (bearer), gsso-reconciler lives in master |
| Required actions | verify e-mail, update password, configure OTP, WebAuthn register, terms (disabled) |
| Token exchange | standard token exchange V2 enabled; per-client permissions from GSSO |
cetatean.json uses the same theme and different settings:
- an MPass SAML identity provider with a first-broker-login flow that matches on IDNP;
- no LDAP;
- password login disabled by default;
- session idle 15 min.
tenant-template.json is a parameterised copy of gstack without federation.
7. Security model
Section titled “7. Security model”| Concern | Measure |
|---|---|
| Console access | BFF; GSSO_* roles; MFA required; idle 15 min; CSRF; CSP |
| Realm scoping | GSSO_REALM_ADMIN carries attribute gsso.realm; server filter on every query and command; tested by authorization matrix |
| Platform scoping | GSSO_PLATFORM_OWNER with attribute gsso.platforma (multi-valued) |
| Reconciler | Service account in master with only manage-*/view-* roles of managed realms (fine-grained admin permissions V2); credentials in secret store; rotated every 90 days |
| Four-eyes | ApprovalPolicy: requester ≠ approver; for sensitive roles two distinct approvers; GSSO roles themselves are sensitive |
| Secrets | Client secrets are generated by Keycloak, shown once in the console, not persisted in GSSO DB |
| Personal data | IDNP encrypted (AES-GCM, key from secret store) and masked; logs scrubbed |
| Events integrity | Hash chain + DB trigger + GLog WORM copy |
| Keys | RS256 realm keys, rotation 90 days with overlap (new active, old passive until max token TTL passes) |
| Supply chain | SBOM (CycloneDX), Trivy, Semgrep, Gitleaks via /gsast; Keycloak image built from pinned digest |
GSSO console roles (realm gstack):
| Role | Sensitive |
|---|---|
GSSO_ADMIN | yes |
GSSO_REALM_ADMIN | yes |
GSSO_PLATFORM_OWNER | yes |
GSSO_APPROVER | yes |
GSSO_HELPDESK | yes |
GSSO_AUDITOR | no |
GSSO_IDNP_VIEW | yes |
GSSO_MFA_REQUIRED | marker |
8. Deployment
Section titled “8. Deployment”8.1 Demo host (current gStack convention)
Section titled “8.1 Demo host (current gStack convention)”- Compose stack
~/devops/gsso/on the shared host. The containers join the externalgstack-webnetwork. - The live
keycloak+postgres-keycloakcontainers are taken over (§11.1): the stock image is swapped for the GSSO image, built on the same version with extensions and theme added. gsso,gsso-gateway,gsso-web,gsso-postgresand, if no shared Kafka is available, a single-node Kafka (KRaft).- vhosts:
sso.gstack.esempla.systems→ keycloak:8080, as today;- new
gsso.gstack.esempla.systems→ gsso-web + gateway. Any edge nginx change requires the owner’s approval, and certificate renewal must be fixed first (NFR-AVL-006).
- Deploy flow: build amd64 images → push to
registry.esempla.systems/govtech/gstack/gsso-*(ordocker save | ssh | docker load) →docker compose up -d --no-deps --force-recreate <svc>. - Memory budget on the ~8 GB host:
- Keycloak 1 GB heap;
- gsso 512 MB;
- gateway 384 MB;
- postgres ×2 256 MB each;
- Kafka 512 MB, if local.
8.2 Kubernetes target (Helm)
Section titled “8.2 Kubernetes target (Helm)”- Keycloak: 2 replicas (Infinispan cluster through the Kubernetes DNS_PING), with PDB and HPA on CPU.
- gsso: 2 replicas. The reconciler is singleton per realm through the advisory lock, so both replicas can run.
- gateway: 2 replicas, sessions in Redis or JDBC.
- PostgreSQL: CloudNativePG, 2 instances each.
- Kafka: the shared Strimzi cluster.
9. Sizing (initial)
Section titled “9. Sizing (initial)”| Item | Estimate |
|---|---|
| Staff users | 5 000 (initial) → 20 000 |
| Peak logins | 50/min typical, 10/s burst at 9:00 |
| Events/day | ≈ 50 000 (logins, token refresh not logged per default, admin) → ≈ 30 MB/day jsonb → 90-day partitions ≈ 3 GB |
| GSSO DB | < 10 GB year one |
| Keycloak DB | < 5 GB (sessions in memory, persistent user sessions feature enabled in 26.x → +1 GB) |
10. Observability
Section titled “10. Observability”- Metrics (Micrometer → Prometheus):
gsso_sync_jobs{state},gsso_reconcile_seconds,gsso_drift_total,gsso_event_lag_seconds,gsso_glog_lag_seconds;- Keycloak
keycloak_logins_total,keycloak_failed_login_attempts_total(metrics SPI); - JVM and HTTP metrics.
- Logs: JSON with trace id; scrubbing filter for
Authorization,access_token,client_secret,password,idnp. - Tracing: OpenTelemetry across gateway → gsso → Keycloak admin calls.
- Alerts: NFR-OBS-004, routed through GNotify.
11. Operations runbook (outline)
Section titled “11. Operations runbook (outline)”11.1 Takeover of the live Keycloak
Section titled “11.1 Takeover of the live Keycloak”- Get owner approval and agree a window. Fix TLS renewal first.
pg_dumpthe Keycloak database and copy it off-host. Export each realm (kc.sh export --realm … --users different_files) for reference.- Build the GSSO Keycloak image on the same version (26.6.3) with extensions and theme added. No baseline is applied yet.
- Swap the image and verify:
- logins for all apps;
- the issuer is unchanged;
- JWKS is unchanged. Rollback = previous image, same database.
- Enable the
gsso-kafkalistener in realminterdictii(admin action), then startgssowith realminterdictiiadopted,IGNORE. - Apply the
gstackandcetateanbaselines (new realms; nothing existing changes).
11.2 Other runbooks
Section titled “11.2 Other runbooks”| Runbook | Steps |
|---|---|
| Keycloak upgrade | Back up the database → run the baseline + reconciler IT suite against the new image → staged rollout → smoke test |
| Key rotation | Add a new RS256 key (higher priority) → wait at least 1 token TTL + refresh → set the old key passive → remove it after the max session |
| Client secret rotation | Console “rotate” → dual-secret grace window → app redeploy → old secret invalidated |
| Break-glass | Use the master emergency admin (sealed credentials, two-person rule); every use is reported |
| Restore | Restore the Keycloak database from backup → run a full reconcile in REPORT → review drift → ENFORCE |
| Certificate | Renew via webroot; alert at 21 days; monthly check job |
12. Testing strategy
Section titled “12. Testing strategy”| Level | Scope | Tooling |
|---|---|---|
| Unit | Grant state machine, approval policy, differ, hash chain, mappers, starter mapping | JUnit 5, AssertJ |
| Integration | Reconciler against real Keycloak, event listener → Kafka → consumer, Liquibase + append-only trigger | Testcontainers (Keycloak 26.6.x with extensions, PostgreSQL, Kafka) and deploy/docker-compose.test.yml (down -v always) |
| Contract | api/v1/app OpenAPI, event schemas (CloudEvents + JSON Schema) | api_audit.py, schema tests |
| E2E | Console flows (register platform, user, grant approval, revoke, drift), SSO across two sample apps, MPass mock | Cypress |
| Security | Authorization matrix (realm/platform scoping, four-eyes), ZAP baseline, headers, SAST/SCA/secrets | /gtestgen, /gtest, /gsast |
| Performance | Login burst, console lists, reconcile of 1 000 clients | /load-test, k6 for Keycloak |
| UI sweep | Dead controls, untranslated keys, a11y | /gfront |
Test scenarios are listed in test-scenarios.md (TS-GSSO-NN) and reuse the shared TC-COM-* cases.
13. Cost estimate
Section titled “13. Cost estimate”| Item | Estimate |
|---|---|
| Demo host | Fits on the existing host (≈ 3 GB additional RAM) |
| Kubernetes target | 3 vCPU / 6 GB requests (Keycloak 2×, gsso 2×, gateway 2×) + 2 PostgreSQL clusters |
| Build effort (S0–S3) | ≈ 14–18 person-weeks (1 backend, 1 frontend, 0.5 Keycloak/DevOps) |
| Migration of existing apps (S4) | ≈ 1–2 person-weeks per pattern-A app, ≈ 0.5 per pattern-B app |