M01 — Foundations
One-line goal: the platform’s infrastructure, guardrails, and contracts are in place such that any subsequent milestone can build on them without re-litigating decisions.
This is the milestone with the highest leverage. Done well, every other milestone benefits. Done sloppily, every other milestone pays for it. Treat it accordingly.
Tracks involved
- Cloud Infra — primary owner. GCP project, GKE, networking, KMS, Terraform, CI/CD.
- Backend — defines the canonical contracts (events, API DTOs) and stands up the hello-world NestJS shell.
- AI Worker — hello-world inference container + GPU-node verification.
- Edge — hello-world supervisor binary + hello-world Python application container.
- Frontend — hello-world Vite SPA + canary auth integration shell.
The infra track produces the foundation everyone else builds on; the other tracks each ship a smoke-test artifact to confirm the foundation works for their workload type.
Dependencies
- CNTXT / GCP Dammam account active with billing — or the documented temporary-project workaround active for dev (see
05-contingencies.md§3). - Decisions already locked: tech stack (
01-architecture-summary.md), cost guardrails (02-cost-guardrails.md), data protection (03-data-protection.md), contracts (04-abstractions-and-contracts.md). Do not re-litigate during this milestone.
Deliverables
1. Cloud project setup
- GCP project
wiqaia-pilot-prod(andwiqaia-pilot-dev,wiqaia-pilot-stagingas separate projects for cleaner blast-radius isolation; each has its own quota + budget). - Billing account linked through CNTXT.
- Organization-level policies enforcing: no public storage buckets without exception; CMEK required for storage; no default service account for new resources; private GKE clusters only.
2. Cost guardrails (per §3 of 02-cost-guardrails.md)
- Project budget: $4,000 USD/month (pilot-prod), $200/month (dev, staging), with alerts at 25 / 50 / 75 / 90 / 100 / 110 / 120%. The pilot-prod cap is sized for one always-warm L4 (~$1,000/mo) plus the rest of the data plane plus headroom — see
docs/plan/02-cost-guardrails.md§7. - Per-service budgets: GPU $1,500 (one always-warm L4 baseline ~$1,000 + surge headroom), egress $200, BigQuery $100, storage $200.
- Quotas set to the pilot profile in §4.1.
billing-alertsPub/Sub topic exists (audit trail destination; no programmatic consumer — L3 auto-kill deferred per decisions).- Daily anomaly-detection alerts enabled.
- Cost-overview dashboard live in Cloud Monitoring.
3. Networking
- One VPC per environment, with subnets per region/zone.
- Private GKE cluster (no public node IPs).
- Cloud NAT for outbound; egress firewall rule blocks everything except a documented allow-list (Pub/Sub, Cloud SQL, Cloud Storage, Secret Manager, KMS, Artifact Registry, logging, monitoring, package registries for image pulls).
- Cloud Armor on the external load balancer (geographic + rate-based rules).
- VPC Service Controls perimeter around the sensitive APIs (KMS, Secret Manager, Cloud Storage) — limits exfil even if a service account leaks.
4. KMS and encryption
- KMS keyring
wiqaia-cmekinme-central2with keys per data class (db,storage-frames,storage-clips,storage-models,logging,bigquery). 90-day rotation enabled. - KMS keyring
wiqaia-app-keys(separate IAM, stricter access) for application-layer KEKs (per03-data-protection.md§5). 30-day rotation enabled. - Verified CMEK enforced on every storage primitive created by Terraform — Terraform policy fails on resources without CMEK.
5. Data plane primitives (empty but ready)
- Cloud SQL Postgres 15 instance, db-custom-2-7680, HA with regional replica, CMEK, private IP only. Empty database
wiqaiawith PostGIS extension installed. - Memorystore Redis 7 M1 standard 4 GB, CMEK, private IP only. Empty.
- Cloud Storage buckets created per
(env, tenant_id, data_class)— for the pilot,wiqaia-pilot-{saudi_airlines_tenant_uuid}-{frames|clips|models|floorplans}. CMEK on each. Lifecycle policies set. - Pub/Sub topics created:
frame-events,analysis-events,tier2-events,alarm-events,alert-events,audit-events,edge-telemetry-events. Per-topic subscriptions with IAM bindings to the consumer service accounts. - BigQuery dataset
wiqaia_analytics, CMEK, with empty tables for analysis events and alerts (matching protobuf shapes from the contracts package). - Secret Manager seeded with placeholder secrets (database connection, JWT signing key, edge bootstrap token, etc.) — actual values populated as needed.
- Artifact Registry repository
wiqaiainme-central2with the standard naming conventionme-central2-docker.pkg.dev/wiqaia-pilot-prod/wiqaia/<app-name>:<tag>.
6. GKE cluster
- Cluster
wiqaia-pilotinme-central2, GKE Standard, multi-zone within the region for HA. - Node pools:
general-cpu— 3×e2-medium, autoscale 3–10.gpu-tier2— 1×g2-standard-8(L4 GPU) always-warm (minReplicas: 1), autoscale 1–2 (max 2 to allow a brief surge during model swap or canary). Note: T4 / A100 / H100 are not available in me-central2; L4 is the only GPU option in-region, only in zonesme-central2-aandme-central2-c. The always-warm policy is required because fires happen year-round and 4-7 minute GPU cold-starts are unacceptable on a fire-detection pipeline.
The earlier 3-tier plan included a separate gpu-tier1 pool. On 2026-05-13 that workload (Tier 0 motion filter + anomaly autoencoder) moved to the edge device as CPU-only ONNX inference — see docs/plan/01-architecture-summary.md §0. There is now exactly one GPU node pool in the cloud.
- Workload Identity Federation enabled (no static service-account keys).
- Network policies enforced (default deny intra-namespace; explicit allow per service).
- Pod Security Standards:
restrictedprofile enforced for application namespaces.
7. Service mesh — Linkerd (self-managed)
Decision recorded in decisions 2026-05-14. Linkerd’s smaller per-pod overhead (~10-30 MB vs Istio’s ~100-200 MB) and operational simplicity beat Istio’s feature richness at our scale; we don’t need traffic-shifting / VirtualService machinery.
- Linkerd deployed in cluster-wide mode. Automatic mTLS for every meshed pod — no per-service policy configuration needed to meet the security baseline.
- Installs via the Linkerd CLI / Helm chart post-cluster-bootstrap (not via Terraform — the
kubernetesprovider is brittle for Linkerd’s CRDs + identity-issuer lifecycle). - Service-to-service authorization policies via Linkerd’s
Server+ServerAuthorizationresources: deny-by-default, explicit allows per app in their deployment manifests.
8. Identity skeleton
- Keycloak deployed on the cluster (private IP, accessed through the mesh gateway). Realm
wiqaiacreated. Roles seeded:super_admin,national_hq_operator,regional_commander,building_admin,building_manager,operator,fd_dispatcher,auditor. - Hello-world test user created in the dev realm only; production users come in M02.
9. The packages/contracts/ package (v0.1.0)
- Protobuf files for the canonical events (
FrameEvent,AnalysisEvent,Tier2AnalysisEvent,AlarmEvent,Alert,EdgeTelemetryEvent,AuditLogEntry). - JSON Schemas for the initial REST DTOs (
UserDto,BuildingDto,CameraDto,AlertDto). - Generated bindings for TS, Python, Go produced by a single
pnpm build(ormake) at the package root. - Driver interfaces (per
04-abstractions-and-contracts.md§5) — PythonProtocolclasses and TSinterfacedefinitions. Stubs only at M01. - Published as a workspace package; importable from every other workspace.
- CHANGELOG.md initialized.
10. CI/CD
Source of truth is our self-hosted Gitea (rizoma/wiqaia at git.rizoma.sa). Gitea Actions is not used — the ops VM is 2 GiB RAM and cannot safely host a runner without OOM risk. Two layers instead:
- Local pre-push gate (now):
scripts/git-hooks/pre-pushruns the same checks CI would — static validation, Terraform fmt + validate,pnpm -r test,pnpm -r typecheck. Install withmake hooks-install(idempotent symlink to the version-controlled hook). Skips heavy checks for docs-only pushes. - Cloud Build CI (planned, separate issue): per-app
cloudbuild.yamlworkflows triggered by Gitea push webhooks. Steps: lint (ESLint / Ruff / golangci-lint), typecheck (tsc / mypy / go vet), test, Docker build per app, push to Artifact Registry onmainmerge, deploy-canary. Turborepo caching backed by GCS in Dammam. Set up when the cloud engineer hire has bandwidth — see the “Set up Cloud Build CI infrastructure” issue in Gitea. - Branch-protection rules on
master(once Cloud Build is live): require passing CI, no force-push. Two-person review when team grows beyond solo.
11. Hello-world deployments
Each app ships a minimum-viable container that proves the toolchain end-to-end:
apps/api— NestJS app with a singleGET /healthzroute that returns{ ok: true, version, env }. Deployed; reachable through the mesh gateway via HTTPS athttps://api.dev.wiqaia.sa/healthz.apps/dashboard— Vite SPA showing “WiqAIa+ — coming online” and a styled “log in” button (no auth flow yet). Deployed; reachable athttps://dev.wiqaia.sa/.apps/fd-view— same minimal Vite SPA, separate origin (https://fd.dev.wiqaia.sa/).apps/edge-dashboard— same minimal Vite SPA but configured to be served by the edge supervisor onhttp://<edge-ip>:8443/over a self-signed cert on the LAN.apps/ai-worker— Python service that accepts a POST/healthzand a POST/echo-inferencethat runs a no-op against an installed model artifact (verifies GPU availability when scaled up).apps/edge-supervisor— Go binary that, when started, registers with the cloud fleet-manager stub and emits aEdgeTelemetryEventto its assigned Pub/Sub topic. systemd unit file shipped.apps/edge-app— Python service that the supervisor starts as a container; logs a “hello from edge” message at startup.
12. Bootstrap install script
infra/edge-bootstrap/install.sh— a single Bash script the customer runs on their Ubuntu box. Does: install Docker, drop the supervisor binary into/usr/local/bin/, install the systemd unit, fetch a one-time onboarding token from a passed argument, register with the cloud, pull and run the hello-world edge-app container.- Hosted at
https://install.wiqaia.sa/edge(Cloud CDN, with version pinning support). - Documented in
infra/edge-bootstrap/README.md.
13. Observability
- Cloud Logging ingesting from all GKE pods, with CMEK on the logs bucket.
- Cloud Monitoring with alerts on: GKE pod crash loops, Cloud SQL CPU > 80%, Memorystore memory > 80%, audit-log chain inconsistency.
- Sentry SaaS project for dev/staging error tracking — synthetic data only.
- Cost-overview dashboard live with the panels listed in
02-cost-guardrails.md§9.
Verification
Concrete observable behaviors. A milestone is not “done” until these pass.
- The hello-world API responds.
curl https://api.dev.wiqaia.sa/healthzreturns 200 with the expected JSON. The request hits Cloud LB → mesh gateway → API pod and is logged with full request context in Cloud Logging. - The hello-world dashboard loads in a browser with proper TLS (HSTS, modern cipher) and Arabic + English language switching working on the static UI shell.
- The hello-world AI worker can run on a GPU. The always-warm gpu-tier2 node is up; running
kubectl execagainst the worker pod confirms CUDA is visible and a no-op inference completes. (No on-demand GPU scaling needed — the pool isminReplicas: 1.) - The edge supervisor registers and reports telemetry. Starting the supervisor on a developer’s Ubuntu VM with a valid token results in the fleet-manager stub recording the registration and the cloud receiving at least one
EdgeTelemetryEventper minute. - A synthetic budget alert lands on
billing-alerts. Publishing a test payload to the topic with"alert_threshold_percent": 100is observable viagcloud pubsub topics ... pull. There’s no programmatic consumer (L3 deferred); the test confirms the audit-trail path works for postmortems. See decisions for why L3 was deferred. - CI passes a smoke PR. Open a PR that modifies one comment in
apps/api/. CI runs lint, typecheck, test, build, push, deploy-canary, and all complete in under 10 minutes. - The contracts package is importable everywhere.
apps/apiimports a generated TS type,apps/ai-workerimports a generated Python pydantic class,apps/edge-supervisorimports a generated Go struct. All compile. - The audit log hash chain is verifiable. Insert 10 synthetic
AuditLogEntryrows; run the chain-verify script; it returns OK. Tamper with one row’schangesfield directly in Postgres; re-run; the script flags the inconsistency. - A non-CMEK storage bucket fails to apply. Attempting
terraform applyon a config that creates a bucket without akms_key_nameresults in a policy-engine failure during plan, not at apply time. - No service account has broader-than-necessary IAM. A manual audit of all service-account bindings confirms least-privilege. (Documented in
infra/iam/audit.md.)
Risks
| Risk | Likelihood | Mitigation |
|---|---|---|
| CNTXT account not active in time | Medium | Temporary GCP project workaround (05-contingencies.md §3); only synthetic data; production deploy still gated on Dammam account |
| GPU quota denied or delayed | Medium | On-prem GPU box pre-ordered as insurance (05-contingencies.md §1) |
| Istio vs Linkerd indecision drags | Low | Time-box to 2 days; default to Istio if no clear winner |
| Keycloak deployment complexity | Low-Med | We accept Keycloak’s operational weight; alternative (SuperTokens) evaluated quickly only if Keycloak blocks |
| Workload Identity Federation friction | Low | Standard pattern; documentation is solid |
| Bootstrap install script breaks on a non-mainstream Ubuntu variant | Low | Target Ubuntu Server 22.04 LTS officially; document support; surface clear errors on unsupported OSes |
Open questions
Istio vs Linkerd for the service mesh.Resolved 2026-05-14: Linkerd, self-managed. See decisions.- Helm vs Kustomize for K8s manifests. Helm leans more toolable; Kustomize is leaner. Default Helm; decide quickly.
Cloud Build vs GitHub Actions.Resolved 2026-05-14: Cloud Build, when we set it up. Source hosting is self-hosted Gitea in-Kingdom; no GitHub involvement. See the “Set up Cloud Build CI infrastructure” issue in Gitea.- Sentry self-hosted vs GlitchTip for production. Defer to M10 (production-ready hardening); Sentry SaaS for dev/staging is fine now.
Exit criteria
M01 is complete when all 10 verification items above pass. Sign off explicitly — append a one-paragraph “M01 complete” entry to the completion log with date and verifier.