Skip to content

Data

Storage layers

Layer Tech Purpose
Transactional AlloyDB (Postgres 15+), one per service OLTP state. Private IP, pgAudit, row-level security. Read replicas on submission-svc and comment-svc for dashboard load.
Files Google Cloud Storage (CMEK) Raw uploads, processed artifacts, signed certificates, PSP exports.
Search / RAG Vertex AI Vector Search SIRP rule corpus, comment library, prior-case retrieval.
Audit / BI BigQuery Append-only, Pub/Sub-sourced, 10-year retention. Auditors read a dedicated restricted dataset.
Cache / sessions Memorystore (Redis) Session state, short-lived caches.
Event bus Pub/Sub Domain events; subscribed to BigQuery sink.
Delayed jobs Cloud Tasks Scheduling not expressible as Temporal timers.

GCS bucket lifecycle

Bucket Contents
siss-uploads Raw PSP uploads — IFC, PDF, DWG, Borang forms. Retained per PDPA rules.
siss-processed Normalized PDFs, extracted artifacts, IFC XKT tiles.
siss-signed Signed Kertas Perakuan and SIGL certificates.
siss-exports PSP-facing downloadable bundles.

All buckets are CMEK-encrypted and accessed through short-lived V4-signed URLs or service-to-service IAM.

Shared identifiers

SISS uses a small set of IDs that trace across every service and every trace span.

ID Format Purpose
tenant_id UUID Issued by GT Console (canonical tenant registry). SISS stores no user or tenant tables of its own. The CMU tenant and each PSP firm tenant are all GT Console tenants.
submission_id ULID, prefix sub_ Stable across the whole lifecycle — pre-consult → Kertas Perakuan.
lot_id / plot_id Masterplan / UDP reference Consumed by ai-svc and bim-svc for zoning lookups.
workflow_id Temporal run ID Bound to every state change for audit replay.

Submission aggregate (ERD)

erDiagram
  SUBMISSIONS ||--o{ SUBMISSION_REVISIONS : "has"
  SUBMISSION_REVISIONS ||--o{ SUBMISSION_FILES : "contains"
  SUBMISSIONS ||--o{ PRECONSULT_CHECKLISTS : "produces"
  SUBMISSIONS ||--o{ SUBMISSION_EVENTS : "emits"

  SUBMISSIONS {
    uuid id PK
    uuid tenant_id
    uuid psp_user_id
    string submission_type
    string lot_id
    string status
    timestamp created_at
    timestamp updated_at
  }
  SUBMISSION_REVISIONS {
    uuid id PK
    uuid submission_id FK
    int revision_no
    uuid created_by
    timestamp created_at
  }
  SUBMISSION_FILES {
    uuid id PK
    uuid revision_id FK
    string file_kind
    string gcs_uri
    string content_type
    bigint size_bytes
    string sha256
    timestamp uploaded_at
  }
  PRECONSULT_CHECKLISTS {
    uuid id PK
    uuid submission_id FK
    uuid revision_id FK
    json items
    string officer_decision
    uuid officer_user_id
    timestamp decided_at
  }
  SUBMISSION_EVENTS {
    uuid id PK
    uuid submission_id FK
    string type
    json payload
    timestamp at
  }

Row-level security

Every query in every service runs under a request-scoped AuthContext (tenant_id, user_id, roles, permissions). RLS policies filter rows by tenant_id and, where applicable, by psp_user_id (for PSPs) or department_id (for ATD / ATL officers) before they reach the application layer. See Security & RBAC for the full grain.

Multi-tenancy

GT Console is the tenant registry

SISS is multi-tenant from day one: CMU is one tenant, and each PSP firm is its own tenant. Tenants are canonical in GT Console; SISS does not maintain its own tenant or user tables. Onboarding additional local authorities later is an activation problem (see "What activation would require" below), not a rewrite.

What's pre-wired today

  • tenant_id is on every table, on every domain event, and on every audit row.
  • AlloyDB row-level security enforces tenant scoping at the query layer — application code can't accidentally leak across tenants.
  • Service-to-service calls carry tenant_id in the signed AuthContext token produced by core-svc on top of GT Console's module-level decision.

What activation would require

  • Tenant provisioning — a workflow that creates the tenant row, seeds roles, registers the SIRP rule corpus, uploads signing certificates, and configures workflow definitions.
  • Per-tenant configuration — SIRP rules, comment templates, workflow shapes, PSP portal branding, notification templates all become tenant-scoped (today they are effectively global because there is only one tenant).
  • Per-tenant identity — each authority federates its own IdP via Identity Platform tenancy (e.g., MyDigital ID for one, ADFS for another).
  • Per-tenant signingsigning-svc holds a separate KMS key set + cert chain per tenant, since each authority has its own legal trust anchor. See Signing.
  • Audit isolation — BigQuery views partitioned by tenant_id; auditors of one tenant must never see another's submissions.
  • Operational isolation — per-tenant SLOs and quotas, noisy-neighbor controls (Pub/Sub flow control, AlloyDB connection pools), per-tenant data-residency where required.

Tradeoff

A single shared instance is cheaper to operate but means one tenant's incident is everyone's incident. Most regulated-vertical SaaS offerings end up providing both shared and dedicated tiers — small authorities share the instance; large ones get their own deployment of the same Helm chart.

Audit retention and PDPA

  • Audit stream — every state-changing domain event plus every auth event, published to Pub/Sub and sunk into BigQuery, append-only, retained 10 years.
  • PDPA subject requests — export and erasure operate on submission_id / user_id. Erasure is tombstone + redact, never physical delete, so audit integrity is preserved.
  • Tamper evidence — daily hash-chain digest of the audit stream is signed by KMS and published to a public transparency log.