@slingr/cli 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/LICENSE.txt +202 -0
  2. package/README.md +490 -319
  3. package/bin/dev.cmd +2 -2
  4. package/bin/dev.js +5 -5
  5. package/bin/run.cmd +2 -2
  6. package/bin/run.js +4 -4
  7. package/bin/slingr +1 -0
  8. package/dist/commands/build.d.ts +20 -0
  9. package/dist/commands/build.d.ts.map +1 -0
  10. package/dist/commands/build.js +206 -0
  11. package/dist/commands/build.js.map +1 -0
  12. package/dist/commands/create-app.d.ts +0 -1
  13. package/dist/commands/create-app.d.ts.map +1 -1
  14. package/dist/commands/create-app.js +38 -57
  15. package/dist/commands/create-app.js.map +1 -1
  16. package/dist/commands/debug.d.ts +28 -0
  17. package/dist/commands/debug.d.ts.map +1 -0
  18. package/dist/commands/debug.js +474 -0
  19. package/dist/commands/debug.js.map +1 -0
  20. package/dist/commands/ds.d.ts +14 -1
  21. package/dist/commands/ds.d.ts.map +1 -1
  22. package/dist/commands/ds.js +450 -121
  23. package/dist/commands/ds.js.map +1 -1
  24. package/dist/commands/gql.d.ts +1 -1
  25. package/dist/commands/gql.d.ts.map +1 -1
  26. package/dist/commands/gql.js +190 -184
  27. package/dist/commands/gql.js.map +1 -1
  28. package/dist/commands/infra/down.d.ts.map +1 -1
  29. package/dist/commands/infra/down.js +8 -7
  30. package/dist/commands/infra/down.js.map +1 -1
  31. package/dist/commands/infra/up.d.ts.map +1 -1
  32. package/dist/commands/infra/up.js +8 -7
  33. package/dist/commands/infra/up.js.map +1 -1
  34. package/dist/commands/infra/update.d.ts +1 -0
  35. package/dist/commands/infra/update.d.ts.map +1 -1
  36. package/dist/commands/infra/update.js +33 -69
  37. package/dist/commands/infra/update.js.map +1 -1
  38. package/dist/commands/run.d.ts +29 -2
  39. package/dist/commands/run.d.ts.map +1 -1
  40. package/dist/commands/run.js +628 -130
  41. package/dist/commands/run.js.map +1 -1
  42. package/dist/commands/setup.d.ts +1 -1
  43. package/dist/commands/setup.d.ts.map +1 -1
  44. package/dist/commands/setup.js +34 -71
  45. package/dist/commands/setup.js.map +1 -1
  46. package/dist/commands/sync-metadata.d.ts +15 -0
  47. package/dist/commands/sync-metadata.d.ts.map +1 -0
  48. package/dist/commands/sync-metadata.js +225 -0
  49. package/dist/commands/sync-metadata.js.map +1 -0
  50. package/dist/commands/users.d.ts +30 -0
  51. package/dist/commands/users.d.ts.map +1 -0
  52. package/dist/commands/users.js +472 -0
  53. package/dist/commands/users.js.map +1 -0
  54. package/dist/commands/views.d.ts +11 -0
  55. package/dist/commands/views.d.ts.map +1 -0
  56. package/dist/commands/views.js +73 -0
  57. package/dist/commands/views.js.map +1 -0
  58. package/dist/projectStructure.d.ts +2 -2
  59. package/dist/projectStructure.d.ts.map +1 -1
  60. package/dist/projectStructure.js +281 -69
  61. package/dist/projectStructure.js.map +1 -1
  62. package/dist/scripts/generate-metadata.d.ts +13 -0
  63. package/dist/scripts/generate-metadata.d.ts.map +1 -0
  64. package/dist/scripts/generate-metadata.js +412 -0
  65. package/dist/scripts/generate-metadata.js.map +1 -0
  66. package/dist/scripts/generate-metadata.ts +498 -0
  67. package/dist/scripts/generate-schema.d.ts +1 -1
  68. package/dist/scripts/generate-schema.js +168 -74
  69. package/dist/scripts/generate-schema.js.map +1 -1
  70. package/dist/scripts/generate-schema.ts +258 -143
  71. package/dist/templates/.env.template +23 -0
  72. package/dist/templates/.firebaserc.template +5 -0
  73. package/dist/templates/.github/copilot-instructions.md.template +652 -17
  74. package/dist/templates/backend/Dockerfile.template +30 -0
  75. package/dist/templates/config/datasource.ts.template +12 -9
  76. package/dist/templates/config/jest.config.ts +30 -30
  77. package/dist/templates/config/jest.setup.ts +1 -1
  78. package/dist/templates/config/tsconfig.json.template +50 -29
  79. package/dist/templates/dataSources/mysql.ts.template +16 -13
  80. package/dist/templates/dataSources/postgres.ts.template +15 -13
  81. package/dist/templates/dataset-generator-script.ts.template +139 -139
  82. package/dist/templates/datasets/mysql-default/.slingr-schema.json.template +5 -0
  83. package/dist/templates/datasets/mysql-default/Address.jsonl.template +3 -3
  84. package/dist/templates/datasets/mysql-default/App.jsonl.template +4 -4
  85. package/dist/templates/datasets/mysql-default/Company.jsonl.template +3 -3
  86. package/dist/templates/datasets/mysql-default/Person.jsonl.template +2 -2
  87. package/dist/templates/datasets/mysql-default/User.jsonl.template +1 -0
  88. package/dist/templates/datasets/mysql-default/instructions.md.template +1 -0
  89. package/dist/templates/datasets/postgres-default/.slingr-schema.json.template +5 -0
  90. package/dist/templates/datasets/postgres-default/Address.jsonl.template +3 -3
  91. package/dist/templates/datasets/postgres-default/App.jsonl.template +4 -4
  92. package/dist/templates/datasets/postgres-default/Company.jsonl.template +3 -3
  93. package/dist/templates/datasets/postgres-default/Person.jsonl.template +2 -2
  94. package/dist/templates/datasets/postgres-default/User.jsonl.template +1 -0
  95. package/dist/templates/datasets/postgres-default/instructions.md.template +1 -0
  96. package/dist/templates/docker-compose.prod-test.yml.template +32 -0
  97. package/dist/templates/docker-compose.yml.template +24 -0
  98. package/dist/templates/docs/app-description.md.template +33 -33
  99. package/dist/templates/firebase.json.template +68 -0
  100. package/dist/templates/frontend/.umirc.ts.template +23 -0
  101. package/dist/templates/frontend/package.json.template +45 -0
  102. package/dist/templates/frontend/public/config.json +6 -0
  103. package/dist/templates/frontend/public/logo.svg +6 -0
  104. package/dist/templates/frontend/src/app.tsx.template +44 -0
  105. package/dist/templates/frontend/src/global.less.template +117 -0
  106. package/dist/templates/frontend/src/layouts/MainLayout.tsx.template +75 -0
  107. package/dist/templates/frontend/src/types/graphql-augmentation.d.ts.template +44 -0
  108. package/dist/templates/frontend/src/views/customViews/user/UserCreateView.tsx.template +18 -0
  109. package/dist/templates/frontend/src/views/customViews/user/UserEditView.tsx.template +29 -0
  110. package/dist/templates/frontend/src/views/customViews/user/UserReadView.tsx.template +24 -0
  111. package/dist/templates/frontend/src/views/customViews/user/UserTableView.tsx.template +38 -0
  112. package/dist/templates/frontend/src/views/customViews/welcome.tsx.template +34 -0
  113. package/dist/templates/frontend/tsconfig.json.template +50 -0
  114. package/dist/templates/gql/codegen.yml.template +25 -25
  115. package/dist/templates/gql/index.ts.template +17 -24
  116. package/dist/templates/gql/operations.graphql.template +30 -30
  117. package/dist/templates/ops/README.md.template +1045 -0
  118. package/dist/templates/ops/cloudbuild.yaml.template +161 -0
  119. package/dist/templates/ops/scripts/_utils.js.template +217 -0
  120. package/dist/templates/ops/scripts/deploy.js.template +145 -0
  121. package/dist/templates/ops/scripts/setup-gcp.js.template +330 -0
  122. package/dist/templates/ops/scripts/setup-secrets.js.template +76 -0
  123. package/dist/templates/ops/scripts/test-prod-local.js.template +49 -0
  124. package/dist/templates/package.json.template +50 -38
  125. package/dist/templates/pnpm-workspace.yaml.template +3 -0
  126. package/dist/templates/prompt-analysis.md.template +110 -110
  127. package/dist/templates/prompt-script-generation.md.template +258 -258
  128. package/dist/templates/src/Address.ts.template +28 -31
  129. package/dist/templates/src/App.ts.template +17 -61
  130. package/dist/templates/src/Company.ts.template +41 -47
  131. package/dist/templates/src/Models.test.ts.template +654 -654
  132. package/dist/templates/src/Person.test.ts.template +289 -289
  133. package/dist/templates/src/Person.ts.template +90 -105
  134. package/dist/templates/src/actions/index.ts.template +11 -11
  135. package/dist/templates/src/auth/permissions.ts.template +34 -0
  136. package/dist/templates/src/data/App.ts.template +48 -0
  137. package/dist/templates/src/data/User.ts.template +35 -0
  138. package/dist/templates/src/types/gql.d.ts.template +17 -17
  139. package/dist/templates/vscode/extensions.json +4 -3
  140. package/dist/templates/vscode/settings.json +17 -11
  141. package/dist/templates/workspace-package.json.template +21 -0
  142. package/dist/utils/buildCache.d.ts +12 -0
  143. package/dist/utils/buildCache.d.ts.map +1 -0
  144. package/dist/utils/buildCache.js +102 -0
  145. package/dist/utils/buildCache.js.map +1 -0
  146. package/dist/utils/checkFramework.d.ts +27 -0
  147. package/dist/utils/checkFramework.d.ts.map +1 -0
  148. package/dist/utils/checkFramework.js +104 -0
  149. package/dist/utils/checkFramework.js.map +1 -0
  150. package/dist/utils/datasourceParser.d.ts +11 -0
  151. package/dist/utils/datasourceParser.d.ts.map +1 -1
  152. package/dist/utils/datasourceParser.js +154 -56
  153. package/dist/utils/datasourceParser.js.map +1 -1
  154. package/dist/utils/dockerManager.d.ts +25 -0
  155. package/dist/utils/dockerManager.d.ts.map +1 -0
  156. package/dist/utils/dockerManager.js +281 -0
  157. package/dist/utils/dockerManager.js.map +1 -0
  158. package/dist/utils/infraFileParser.d.ts +26 -0
  159. package/dist/utils/infraFileParser.d.ts.map +1 -0
  160. package/dist/utils/infraFileParser.js +75 -0
  161. package/dist/utils/infraFileParser.js.map +1 -0
  162. package/dist/utils/jsonlLoader.d.ts +91 -12
  163. package/dist/utils/jsonlLoader.d.ts.map +1 -1
  164. package/dist/utils/jsonlLoader.js +674 -63
  165. package/dist/utils/jsonlLoader.js.map +1 -1
  166. package/dist/utils/model-analyzer.d.ts.map +1 -1
  167. package/dist/utils/model-analyzer.js +67 -13
  168. package/dist/utils/model-analyzer.js.map +1 -1
  169. package/dist/utils/userManagement.d.ts +57 -0
  170. package/dist/utils/userManagement.d.ts.map +1 -0
  171. package/dist/utils/userManagement.js +288 -0
  172. package/dist/utils/userManagement.js.map +1 -0
  173. package/dist/utils/viewsGenerator.d.ts +15 -0
  174. package/dist/utils/viewsGenerator.d.ts.map +1 -0
  175. package/dist/utils/viewsGenerator.js +311 -0
  176. package/dist/utils/viewsGenerator.js.map +1 -0
  177. package/oclif.manifest.json +445 -20
  178. package/package.json +29 -26
  179. package/src/templates/.env.template +23 -0
  180. package/src/templates/.firebaserc.template +5 -0
  181. package/src/templates/.github/copilot-instructions.md.template +652 -17
  182. package/src/templates/backend/Dockerfile.template +30 -0
  183. package/src/templates/config/datasource.ts.template +12 -9
  184. package/src/templates/config/jest.config.ts +30 -30
  185. package/src/templates/config/jest.setup.ts +1 -1
  186. package/src/templates/config/tsconfig.json.template +50 -29
  187. package/src/templates/dataSources/mysql.ts.template +16 -13
  188. package/src/templates/dataSources/postgres.ts.template +15 -13
  189. package/src/templates/dataset-generator-script.ts.template +139 -139
  190. package/src/templates/datasets/mysql-default/.slingr-schema.json.template +5 -0
  191. package/src/templates/datasets/mysql-default/Address.jsonl.template +3 -3
  192. package/src/templates/datasets/mysql-default/App.jsonl.template +4 -4
  193. package/src/templates/datasets/mysql-default/Company.jsonl.template +3 -3
  194. package/src/templates/datasets/mysql-default/Person.jsonl.template +2 -2
  195. package/src/templates/datasets/mysql-default/User.jsonl.template +1 -0
  196. package/src/templates/datasets/mysql-default/instructions.md.template +1 -0
  197. package/src/templates/datasets/postgres-default/.slingr-schema.json.template +5 -0
  198. package/src/templates/datasets/postgres-default/Address.jsonl.template +3 -3
  199. package/src/templates/datasets/postgres-default/App.jsonl.template +4 -4
  200. package/src/templates/datasets/postgres-default/Company.jsonl.template +3 -3
  201. package/src/templates/datasets/postgres-default/Person.jsonl.template +2 -2
  202. package/src/templates/datasets/postgres-default/User.jsonl.template +1 -0
  203. package/src/templates/datasets/postgres-default/instructions.md.template +1 -0
  204. package/src/templates/docker-compose.prod-test.yml.template +32 -0
  205. package/src/templates/docker-compose.yml.template +24 -0
  206. package/src/templates/docs/app-description.md.template +33 -33
  207. package/src/templates/firebase.json.template +68 -0
  208. package/src/templates/frontend/.umirc.ts.template +23 -0
  209. package/src/templates/frontend/package.json.template +45 -0
  210. package/src/templates/frontend/public/config.json +6 -0
  211. package/src/templates/frontend/public/logo.svg +6 -0
  212. package/src/templates/frontend/src/app.tsx.template +44 -0
  213. package/src/templates/frontend/src/global.less.template +117 -0
  214. package/src/templates/frontend/src/layouts/MainLayout.tsx.template +75 -0
  215. package/src/templates/frontend/src/types/graphql-augmentation.d.ts.template +44 -0
  216. package/src/templates/frontend/src/views/customViews/user/UserCreateView.tsx.template +18 -0
  217. package/src/templates/frontend/src/views/customViews/user/UserEditView.tsx.template +29 -0
  218. package/src/templates/frontend/src/views/customViews/user/UserReadView.tsx.template +24 -0
  219. package/src/templates/frontend/src/views/customViews/user/UserTableView.tsx.template +38 -0
  220. package/src/templates/frontend/src/views/customViews/welcome.tsx.template +34 -0
  221. package/src/templates/frontend/tsconfig.json.template +50 -0
  222. package/src/templates/gql/codegen.yml.template +25 -25
  223. package/src/templates/gql/index.ts.template +17 -24
  224. package/src/templates/gql/operations.graphql.template +30 -30
  225. package/src/templates/ops/README.md.template +1045 -0
  226. package/src/templates/ops/cloudbuild.yaml.template +161 -0
  227. package/src/templates/ops/scripts/_utils.js.template +217 -0
  228. package/src/templates/ops/scripts/deploy.js.template +145 -0
  229. package/src/templates/ops/scripts/setup-gcp.js.template +330 -0
  230. package/src/templates/ops/scripts/setup-secrets.js.template +76 -0
  231. package/src/templates/ops/scripts/test-prod-local.js.template +49 -0
  232. package/src/templates/package.json.template +50 -38
  233. package/src/templates/pnpm-workspace.yaml.template +3 -0
  234. package/src/templates/prompt-analysis.md.template +110 -110
  235. package/src/templates/prompt-script-generation.md.template +258 -258
  236. package/src/templates/src/Address.ts.template +28 -31
  237. package/src/templates/src/App.ts.template +17 -61
  238. package/src/templates/src/Company.ts.template +41 -47
  239. package/src/templates/src/Models.test.ts.template +654 -654
  240. package/src/templates/src/Person.test.ts.template +289 -289
  241. package/src/templates/src/Person.ts.template +90 -105
  242. package/src/templates/src/actions/index.ts.template +11 -11
  243. package/src/templates/src/auth/permissions.ts.template +34 -0
  244. package/src/templates/src/data/App.ts.template +48 -0
  245. package/src/templates/src/data/User.ts.template +35 -0
  246. package/src/templates/src/types/gql.d.ts.template +17 -17
  247. package/src/templates/vscode/extensions.json +4 -3
  248. package/src/templates/vscode/settings.json +17 -11
  249. package/src/templates/workspace-package.json.template +21 -0
  250. package/dist/templates/src/index.ts +0 -66
  251. package/src/templates/src/index.ts +0 -66
@@ -0,0 +1,1045 @@
1
+ # GCP Deployment Guide — Slingr Framework Apps
2
+
3
+ A step-by-step guide for deploying **{{APP_NAME}}** (a Slingr framework app) to Google Cloud Platform.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ - [GCP Deployment Guide — Slingr Framework Apps](#gcp-deployment-guide--slingr-framework-apps)
10
+ - [Table of Contents](#table-of-contents)
11
+ - [Architecture Overview](#architecture-overview)
12
+ - [Prerequisites](#prerequisites)
13
+ - [Tools](#tools)
14
+ - [GCP Setup](#gcp-setup)
15
+ - [Required GCP IAM Permissions](#required-gcp-iam-permissions)
16
+ - [File Structure Convention](#file-structure-convention)
17
+ - [Script Execution Order](#script-execution-order)
18
+ - [Adapting the Scripts for Your App](#adapting-the-scripts-for-your-app)
19
+ - [Variable mapping](#variable-mapping)
20
+ - [Step 1 — Backend Dockerfile](#step-1--backend-dockerfile)
21
+ - [Build context](#build-context)
22
+ - [Step 2 — Local Production Smoke Testing](#step-2--local-production-smoke-testing)
23
+ - [Quick start](#quick-start)
24
+ - [Manual step-by-step](#manual-step-by-step)
25
+ - [What to verify](#what-to-verify)
26
+ - [Iterating quickly](#iterating-quickly)
27
+ - [Step 3 — One-Time GCP Infrastructure Setup](#step-3--one-time-gcp-infrastructure-setup)
28
+ - [What it creates](#what-it-creates)
29
+ - [After running](#after-running)
30
+ - [Step 4 — Secrets Management](#step-4--secrets-management)
31
+ - [Rotating secrets later](#rotating-secrets-later)
32
+ - [Step 5 — Cloud Build Trigger (Automated Deploys)](#step-5--cloud-build-trigger-automated-deploys)
33
+ - [Connect the GitHub repository (one-time)](#connect-the-github-repository-one-time)
34
+ - [Create the trigger](#create-the-trigger)
35
+ - [cloudbuild.yaml structure](#cloudbuildyaml-structure)
36
+ - [Step 6 — Manual Deployment](#step-6--manual-deployment)
37
+ - [Step 7 — DNS \& HTTPS Setup (Optional)](#step-7--dns--https-setup-optional)
38
+ - [What setup-domain.js does](#what-setup-domainjs-does)
39
+ - [After running setup-domain.js](#after-running-setup-domainjs)
40
+ - [Manual equivalent (no script)](#manual-equivalent-no-script)
41
+ - [Networking](#networking)
42
+ - [What was set up](#what-was-set-up)
43
+ - [Cloud SQL connectivity](#cloud-sql-connectivity)
44
+ - [Verifying the networking setup](#verifying-the-networking-setup)
45
+ - [Frontend Hosting](#frontend-hosting)
46
+ - [URL routing](#url-routing)
47
+ - [Manual frontend deploy](#manual-frontend-deploy)
48
+ - [CDN cache invalidation](#cdn-cache-invalidation)
49
+ - [Database Schema Sync](#database-schema-sync)
50
+ - [Default Admin User](#default-admin-user)
51
+ - [Logging \& Observability](#logging--observability)
52
+ - [View backend logs](#view-backend-logs)
53
+ - [Stream live logs](#stream-live-logs)
54
+ - [Useful log filters](#useful-log-filters)
55
+ - [Cloud Monitoring — uptime check](#cloud-monitoring--uptime-check)
56
+ - [Cloud Monitoring — error rate alert](#cloud-monitoring--error-rate-alert)
57
+ - [IAM \& Service Accounts Reference](#iam--service-accounts-reference)
58
+ - [Runtime service account (`<app>-backend`)](#runtime-service-account-app-backend)
59
+ - [Cloud Build service account (CI/CD)](#cloud-build-service-account-cicd)
60
+ - [Ongoing Operations](#ongoing-operations)
61
+ - [Roll back to a previous revision](#roll-back-to-a-previous-revision)
62
+ - [Scale to zero immediately](#scale-to-zero-immediately)
63
+ - [Connect to Cloud SQL from your local machine](#connect-to-cloud-sql-from-your-local-machine)
64
+ - [Troubleshooting](#troubleshooting)
65
+ - [Backend fails to connect to Cloud SQL](#backend-fails-to-connect-to-cloud-sql)
66
+ - [Cloud Run service fails to start](#cloud-run-service-fails-to-start)
67
+ - [Schema sync job exits non-zero](#schema-sync-job-exits-non-zero)
68
+ - [Docker build fails: `cannot find module '@slingr/framework-backend'`](#docker-build-fails-cannot-find-module-slingr-frameworkbackend)
69
+ - [Docker push fails: Unauthenticated request](#docker-push-fails-unauthenticated-request)
70
+ - [Cloud SQL Proxy is not recognized on local machine](#cloud-sql-proxy-is-not-recognized-on-local-machine)
71
+ - [Frontend not updating after deploy](#frontend-not-updating-after-deploy)
72
+ - [Load Balancer returns 404 for frontend routes](#load-balancer-returns-404-for-frontend-routes)
73
+ - [SSL certificate stuck in PROVISIONING](#ssl-certificate-stuck-in-provisioning)
74
+
75
+ ---
76
+
77
+ ## Architecture Overview
78
+
79
+ ```
80
+ ┌─────────────────────────────────────────────────────────────────┐
81
+ │ Users (browser) │
82
+ └───────────────────────────┬─────────────────────────────────────┘
83
+ │ HTTP/HTTPS
84
+ ┌─────────────▼──────────────┐
85
+ │ Cloud HTTP(S) Load Balancer │
86
+ │ /graphql → Cloud Run NEG │
87
+ │ /auth/* → Cloud Run NEG │
88
+ │ /** → GCS bucket CDN │
89
+ └──────┬──────────────┬───────┘
90
+ │ │
91
+ ┌─────────────▼──┐ ┌──────▼──────────────┐
92
+ │ Cloud Run │ │ GCS Bucket (CDN) │
93
+ │ <app>-backend │ │ React SPA static │
94
+ └────────┬────────┘ └─────────────────────┘
95
+ │ Cloud SQL Auth Proxy (IAM, encrypted)
96
+ ┌────────▼────────┐
97
+ │ Cloud SQL │ PostgreSQL 15
98
+ │ <app>-db │
99
+ └─────────────────┘
100
+
101
+ Secrets → Secret Manager (DB password, JWT secret)
102
+ Images → Artifact Registry (Docker)
103
+ CI/CD → Cloud Build (triggered on push to a branch)
104
+ ```
105
+
106
+ **Why this architecture:**
107
+
108
+ | Service | Choice | Reason |
109
+ |---|---|---|
110
+ | Backend | Cloud Run | Serverless, scales to zero, $0 when idle |
111
+ | Frontend | GCS + Cloud LB | CDN-backed static hosting, no extra IAM roles |
112
+ | Database | Cloud SQL (PostgreSQL 15) | Managed, automated backups, point-in-time recovery |
113
+ | DB connectivity | Cloud SQL Auth Proxy | Native Cloud Run integration, no VPC connector needed |
114
+ | Secrets | Secret Manager | Audit trail, rotation support, IAM-scoped access |
115
+
116
+ ---
117
+
118
+ ## Prerequisites
119
+
120
+ ### Tools
121
+
122
+ | Tool | Min Version | Install | Purpose |
123
+ |---|---|---|---|
124
+ | **gcloud CLI** | >= 450.0.0 | [Install guide](https://cloud.google.com/sdk/docs/install) | GCP resource management |
125
+ | **Docker** | >= 24.0 | [Install guide](https://docs.docker.com/get-docker/) | Build backend container image |
126
+ | **Node.js** | >= 20.0 | [Install guide](https://nodejs.org/) | Build frontend, run CLI commands |
127
+ | **pnpm** | >= 8.0 | `npm install -g pnpm` | Monorepo workspace dependency management |
128
+ | **Cloud SQL Auth Proxy** | latest | `gcloud components install cloud-sql-proxy` | Local DB connection for seeding |
129
+ | **gsutil** | bundled | Included in gcloud CLI | Upload frontend files to GCS |
130
+
131
+ ```bash
132
+ # Verify all dependencies
133
+ gcloud --version # >= 450.0.0
134
+ docker --version # >= 24.0
135
+ node --version # >= 20.0
136
+ pnpm --version # >= 8.0
137
+ cloud-sql-proxy --version
138
+ gsutil --version
139
+ ```
140
+
141
+ ### GCP Setup
142
+
143
+ - GCP project created with billing enabled
144
+ - Required IAM roles on the project (see next section)
145
+ - `gcloud init` — sets your authenticated account and default project on this machine
146
+ - `gcloud auth application-default login` — sets Application Default Credentials for local SDK usage
147
+ - `gcloud auth configure-docker REGION-docker.pkg.dev` — authenticates Docker to push images to Artifact Registry
148
+
149
+ > **Important:** `setup-gcp.js` and `setup-secrets.js` are **project-level** bootstrap steps — run once per GCP project. `gcloud init`, `gcloud auth ...`, and installing the gcloud CLI are **machine-level** prerequisites. If you switch machines, install `gcloud` and authenticate again before running any scripts.
150
+
151
+ If the project was already provisioned from another machine:
152
+
153
+ ```bash
154
+ # 1. Install Google Cloud CLI on this machine
155
+ # https://cloud.google.com/sdk/docs/install
156
+
157
+ # 2. Authenticate this machine
158
+ gcloud init
159
+ gcloud auth application-default login
160
+
161
+ # 3. Install Cloud SQL Auth Proxy
162
+ gcloud components install cloud-sql-proxy
163
+ cloud-sql-proxy --version
164
+
165
+ # 4. Authenticate Docker to Artifact Registry
166
+ gcloud auth configure-docker us-central1-docker.pkg.dev
167
+
168
+ # 5. Deploy (run from your app root)
169
+ node ops/scripts/deploy.js
170
+ ```
171
+
172
+ ### Required GCP IAM Permissions
173
+
174
+ The human (or CI service account) running the setup and deployment scripts must have the following roles on the GCP project. The simplest option is `roles/owner`. For least-privilege setups, grant the specific roles:
175
+
176
+ | Role | Required for |
177
+ |---|---|
178
+ | `roles/resourcemanager.projectIamAdmin` | Granting IAM bindings to service accounts in `setup-gcp.js` |
179
+ | `roles/iam.serviceAccountAdmin` | Creating the backend service account |
180
+ | `roles/compute.admin` | Creating the Load Balancer (NEG, URL map, forwarding rules, backend services) |
181
+ | `roles/cloudsql.admin` | Creating the Cloud SQL instance, database, and setting user passwords |
182
+ | `roles/artifactregistry.admin` | Creating the Artifact Registry repository |
183
+ | `roles/run.admin` | Deploying Cloud Run services and jobs |
184
+ | `roles/secretmanager.admin` | Creating and versioning secrets in Secret Manager |
185
+ | `roles/storage.admin` | Creating and configuring the GCS bucket |
186
+ | `roles/serviceusage.serviceUsageAdmin` | Enabling GCP APIs |
187
+ | `roles/cloudbuild.builds.editor` | Creating Cloud Build triggers |
188
+
189
+ ---
190
+
191
+ ## File Structure Convention
192
+
193
+ Each app should have an `ops/` directory mirroring this layout:
194
+
195
+ ```
196
+ apps/<your-app>/
197
+ ├── backend/
198
+ │ └── Dockerfile # Multi-stage build (see Step 1)
199
+ ├── frontend/
200
+ │ └── src/services/GraphQLClientService.ts
201
+ └── ops/
202
+ ├── README.md # This guide
203
+ ├── cloudbuild.yaml # Cloud Build CI/CD pipeline
204
+ ├── nginx/
205
+ │ └── prod-test.conf # nginx config for local smoke testing
206
+ └── scripts/
207
+ ├── setup-gcp.js # One-time infrastructure setup
208
+ ├── setup-secrets.js # Secret Manager credential management
209
+ ├── setup-domain.js # Custom domain + HTTPS setup (optional)
210
+ ├── deploy.js # Manual deployment script
211
+ └── test-prod-local.js # Local production smoke test
212
+ ```
213
+
214
+ The scripts in `ops/scripts/` have been pre-configured for **{{APP_NAME}}**. No manual variable substitution is needed.
215
+
216
+ ---
217
+
218
+ ## Script Execution Order
219
+
220
+ The scripts are designed to be run in this order. Each step builds on the previous one.
221
+
222
+ ```
223
+ 1. (dockerfile) Step 1: Backend Dockerfile — copy and adapt the Dockerfile
224
+ 2. test-prod-local.js Step 2: Verify the full production stack runs correctly in Docker locally
225
+ 3. setup-gcp.js Step 3: Create all GCP infrastructure (run once per project)
226
+ 4. setup-secrets.js Step 4: Store DB password and JWT secret in Secret Manager
227
+ 5. (trigger creation) Step 5: Create the Cloud Build trigger for automated deploys
228
+ 6. deploy.js Step 6: First manual deployment to GCP
229
+ 7. setup-domain.js Step 7: (Optional) Add a custom domain and HTTPS
230
+ ```
231
+
232
+ > **`setup-domain.js` is optional and can be run any time after `setup-gcp.js`** — It requires that the Load Balancer already exists (created by `setup-gcp.js`) but is otherwise independent of deployment state.
233
+
234
+ > **You do not need a domain to deploy.** Without `setup-domain.js`, the app is accessible over HTTP on the Load Balancer IP that `setup-gcp.js` prints at the end. HTTPS with a custom domain is an optional step for production environments.
235
+
236
+ ---
237
+
238
+ ## GCP Resource Naming Reference
239
+
240
+ The scripts use `{{APP_NAME}}` as the base identifier for all GCP resource names. Here is the full mapping for reference:
241
+
242
+ ### Variable mapping
243
+
244
+ | Variable | Description | Value for {{APP_NAME}} |
245
+ |---|---|---|
246
+ | `APP_SHORT` | Short identifier used as a prefix for every GCP resource name. Keep it lowercase, hyphen-separated, and unique within the project. | `<your-app-short>` |
247
+ | `DB_INSTANCE_NAME` | Name of the Cloud SQL instance. One instance can host multiple databases. | `{{APP_NAME}}-db` |
248
+ | `DB_NAME` | Database name. PostgreSQL database inside the instance. Used as `DB_NAME` env var at runtime. | `{{APP_NAME}}` |
249
+ | `DB_USER` | PostgreSQL user the backend connects as. | `postgres` | superuser is used for simplicity |
250
+ | `AR_REPO_NAME` | Shared Docker registry across all apps in the project. | `slingr-apps` || `APP_SHORT` | Short identifier used as a prefix for every GCP resource name. Keep it lowercase, hyphen-separated, and unique within the project. | `<your-app-short>` |
251
+ | `BACKEND_SA_NAME` | Backend service account | Runtime identity for Cloud Run. Granted minimum roles. | `{{APP_NAME}}-backend` |
252
+ | `BACKEND_SERVICE` | Cloud Run service | The deployed backend service. Also used as the Docker image name. | `{{APP_NAME}}-backend` |
253
+ | `FRONTEND_BUCKET` | GCS bucket | Hosts the React SPA static files. Globally unique. | `PROJECT_ID-{{APP_NAME}}-frontend` |
254
+ | `NEG_NAME` | Serverless NEG | Connects the Load Balancer to Cloud Run. | `{{APP_NAME}}-backend-neg` |
255
+ | `URL_MAP_NAME` | Path-based routing (`/graphql` → Cloud Run, `/**` → GCS). | `{{APP_NAME}}-url-map` |
256
+ | `HTTP_PROXY_NAME` | Routes HTTP requests into the URL map. | `{{APP_NAME}}-http-proxy` |
257
+ | `FWD_RULE_NAME` | Port 80, assigns public IP. | `{{APP_NAME}}-http-rule` |
258
+ | `DB_SECRET_NAME` | Cloud SQL `postgres` password in Secret Manager. | `{{APP_NAME}}-db-password` |
259
+ | `JWT_SECRET_NAME` | JWT signing key in Secret Manager. | `{{APP_NAME}}-jwt-secret` |
260
+
261
+
262
+ ---
263
+
264
+ ## Step 1 — Backend Dockerfile
265
+
266
+ The `backend/Dockerfile` has been generated for your app. It uses a two-stage build:
267
+
268
+ 1. **Stage 1 (builder):** Installs dependencies (`npm install`), compiles TypeScript (`npm run build`).
269
+ 2. **Stage 2 (runtime):** Copies only `dist/` and `node_modules/`. Exposes port 8080. Starts with `node dist/src/App.js`.
270
+
271
+ ### Build context
272
+
273
+ The build context is the **app root directory** (the folder containing `backend/` and `frontend/`):
274
+
275
+ ```bash
276
+ # Run from your app root
277
+ docker build \
278
+ -f backend/Dockerfile \
279
+ -t REGION-docker.pkg.dev/PROJECT_ID/slingr-apps/{{APP_NAME}}-backend:latest \
280
+ .
281
+ ```
282
+
283
+ The Cloud Build pipeline and `deploy.js` enforce this automatically.
284
+
285
+ ---
286
+
287
+ ## Step 2 — Local Production Smoke Testing
288
+
289
+ Before touching GCP, run the full production stack locally. This catches environment-specific bugs that only appear in production builds — minified bundles, URL routing, Docker runtime config — without creating any cloud resources.
290
+
291
+ The local stack mirrors the GCP topology exactly:
292
+
293
+ ```
294
+ Browser
295
+ └─► nginx :8080
296
+ ├─ /graphql → backend:3000 (Node.js container)
297
+ ├─ /auth/* → backend:3000
298
+ └─ /** → React SPA (frontend/dist/)
299
+ backend:3000
300
+ └─ mainDs-db:5432 (PostgreSQL 15)
301
+ ```
302
+
303
+ ### Quick start
304
+
305
+ Run from the **app root** (the directory containing `backend/`, `frontend/`, and `ops/`):
306
+
307
+ ```bash
308
+ node ops/scripts/test-prod-local.js
309
+ ```
310
+
311
+ Then open **http://localhost:8080** in your browser.
312
+
313
+ The script performs these steps in order:
314
+
315
+ 1. **Framework build** — skipped in standalone mode (framework is an npm dependency); runs `pnpm run build:all` only when inside the framework monorepo
316
+ 2. `npx slingr sync-metadata` — regenerates `frontend/generated/` (view registry, GraphQL types)
317
+ 3. `npm run build` (in `frontend/`) — produces a minified production bundle in `frontend/dist/`
318
+ 4. `docker compose up --build` — builds the backend image and starts all three containers
319
+
320
+ In monorepo mode, skip rebuilding the framework if only app code changed:
321
+ ```bash
322
+ SKIP_FRAMEWORK_BUILD=1 node ops/scripts/test-prod-local.js
323
+ ```
324
+
325
+ ### Manual step-by-step
326
+
327
+ Run from the **app root**:
328
+
329
+ ```bash
330
+ # 1. (Monorepo only) Build the framework and CLI from source
331
+ # Skip this in standalone mode — the framework is in node_modules
332
+ # pnpm run build:all
333
+
334
+ # 2. Regenerate view registry and GraphQL types
335
+ npx slingr sync-metadata
336
+
337
+ # 3. Build the production frontend bundle
338
+ (cd frontend && npm run build)
339
+
340
+ # 4. Start the full stack
341
+ docker compose \
342
+ -f docker-compose.yml \
343
+ -f docker-compose.prod-test.yml \
344
+ up --build
345
+
346
+ # Open http://localhost:8080
347
+ ```
348
+
349
+ To tear it down and clean up volumes:
350
+
351
+ ```bash
352
+ docker compose \
353
+ -f docker-compose.yml \
354
+ -f docker-compose.prod-test.yml \
355
+ down -v
356
+ ```
357
+
358
+ ### What to verify
359
+
360
+ | Check | How |
361
+ |---|---|
362
+ | App loads at `http://localhost:8080` | No blank page, no console errors on startup |
363
+ | Login works | Authenticate with the seeded user (`sys@app.com`) |
364
+ | Toolbar buttons navigate correctly | Click every toolbar button — a `View class "X" is not registered` error means class name minification is breaking the view registry |
365
+ | Dashboard loads without errors | Open the dashboard — a `Failed to construct 'URL': Invalid URL` error means `GraphQLClientService.baseUrl` is a relative path that `graphql-request` cannot handle |
366
+ | Create / update forms submit | Submit a create form — `Runtime Object type "ExpectedErrorType" is not a possible type` means the GraphQL union type is missing `ExpectedErrorType` in the schema |
367
+ | Backend logs look clean | `docker compose logs backend` — no startup crashes |
368
+
369
+ ### Iterating quickly
370
+
371
+ After fixing a frontend issue (run from app root):
372
+
373
+ ```bash
374
+ (cd frontend && npm run build)
375
+
376
+ docker compose \
377
+ -f docker-compose.yml \
378
+ -f docker-compose.prod-test.yml \
379
+ restart frontend
380
+ ```
381
+
382
+ After fixing a backend issue (run from app root):
383
+
384
+ ```bash
385
+ docker compose \
386
+ -f docker-compose.yml \
387
+ -f docker-compose.prod-test.yml \
388
+ up --build backend
389
+ ```
390
+
391
+ ---
392
+
393
+ ## Step 3 — One-Time GCP Infrastructure Setup
394
+
395
+ Run once per GCP project. The script is idempotent — safe to re-run.
396
+
397
+ Run from the **app root**:
398
+
399
+ ```bash
400
+ node ops/scripts/setup-gcp.js
401
+
402
+ # Override project or region:
403
+ PROJECT_ID=my-project REGION=europe-west1 node ops/scripts/setup-gcp.js
404
+ ```
405
+
406
+ ### What it creates
407
+
408
+ | Resource | Example name | Notes |
409
+ |---|---|---|
410
+ | APIs enabled | Cloud Run, Cloud SQL, Artifact Registry, Cloud Build, Secret Manager, Cloud Storage, Compute Engine | ~1 minute on first run |
411
+ | Artifact Registry repo | `slingr-apps` | Shared Docker registry across all apps in the project |
412
+ | Cloud SQL instance | `<app>-db` | PostgreSQL 15, `db-f1-micro` tier |
413
+ | Cloud SQL database | `<your_db_name>` | Created inside the instance |
414
+ | Cloud SQL user | `postgres` | Password set interactively |
415
+ | Backend service account | `<app>-backend@PROJECT.iam.gserviceaccount.com` | Runtime identity for Cloud Run |
416
+ | IAM bindings (backend SA) | `roles/cloudsql.client`, `roles/secretmanager.secretAccessor` | Minimum required at runtime |
417
+ | IAM bindings (Cloud Build SA) | `roles/run.admin`, `roles/iam.serviceAccountUser`, `roles/secretmanager.secretAccessor`, `roles/artifactregistry.writer`, `roles/cloudsql.client`, `roles/storage.objectAdmin` | Required for automated CI/CD |
418
+ | GCS bucket | `PROJECT_ID-<app>-frontend` | Public, SPA routing via `index.html` |
419
+ | Serverless NEG | `<app>-backend-neg` | Routes Load Balancer traffic to Cloud Run |
420
+ | Backend service | `<app>-backend-service` | Wraps the NEG |
421
+ | Backend bucket | `<app>-frontend-bucket` | CDN-backed, wraps the GCS bucket |
422
+ | URL map | `<app>-url-map` | `/graphql` and `/auth/*` → Cloud Run; `/**` → GCS |
423
+ | HTTP proxy + forwarding rule | `<app>-http-proxy`, `<app>-http-rule` | Port 80; see [Step 7](#step-7--dns--https-setup-optional) to add HTTPS |
424
+
425
+ ### After running
426
+
427
+ The script prints the Load Balancer IP. Note it — this is the app's public IP until you configure a custom domain.
428
+
429
+ ---
430
+
431
+ ## Step 4 — Secrets Management
432
+
433
+ Run after `setup-gcp.js`. Can be re-run at any time to rotate credentials.
434
+
435
+ Run from the **app root**:
436
+
437
+ ```bash
438
+ node ops/scripts/setup-secrets.js
439
+
440
+ # Override project:
441
+ PROJECT_ID=my-project node ops/scripts/setup-secrets.js
442
+ ```
443
+
444
+ The script prompts for:
445
+
446
+ | Secret name | What to enter |
447
+ |---|---|
448
+ | `<app>-db-password` | The password you set on the Cloud SQL `postgres` user during `setup-gcp.js` |
449
+ | `<app>-jwt-secret` | Any long random string — press Enter to auto-generate |
450
+
451
+ Both secrets are created in Secret Manager and the backend service account is granted access to each.
452
+
453
+ ### Rotating secrets later
454
+
455
+ Re-run the script — it adds a new secret version automatically. Cloud Run picks up the new version on the next cold start. To force immediate pickup:
456
+
457
+ ```bash
458
+ gcloud run services update <app>-backend \
459
+ --region=REGION \
460
+ --no-traffic \
461
+ --project=PROJECT_ID
462
+ ```
463
+
464
+ ---
465
+
466
+ ## Step 5 — Cloud Build Trigger (Automated Deploys)
467
+
468
+ ### Connect the GitHub repository (one-time)
469
+
470
+ Before creating the trigger, connect your repo to Cloud Build:
471
+
472
+ ```
473
+ https://console.cloud.google.com/cloud-build/triggers/connect
474
+ ```
475
+
476
+ Or via CLI:
477
+
478
+ ```bash
479
+ gcloud builds connections create github <CONNECTION_NAME> --region=$REGION
480
+ gcloud builds repositories create <REPO_NAME> \
481
+ --remote-uri=https://github.com/<ORG>/<REPO>.git \
482
+ --connection=<CONNECTION_NAME> \
483
+ --region=$REGION
484
+ ```
485
+
486
+ ### Create the trigger
487
+
488
+ ```bash
489
+ PROJECT_ID="your-project-id"
490
+ REGION="us-central1"
491
+ GITHUB_ORG="your-github-org"
492
+ APP="{{APP_NAME}}"
493
+ DB_NAME="{{APP_NAME}}"
494
+
495
+ gcloud builds triggers create github \
496
+ --project="$PROJECT_ID" \
497
+ --name="${APP}-deploy-develop" \
498
+ --repo-name="deployment-support" \
499
+ --repo-owner="$GITHUB_ORG" \
500
+ --branch-pattern="^develop$" \
501
+ --build-config="ops/cloudbuild.yaml" \
502
+ --substitutions="\
503
+ _PROJECT_ID=$PROJECT_ID,\
504
+ _REGION=$REGION,\
505
+ _AR_REPO=slingr-apps,\
506
+ _BACKEND_SERVICE=${APP}-backend,\
507
+ _BACKEND_SA=${APP}-backend@${PROJECT_ID}.iam.gserviceaccount.com,\
508
+ _CLOUDSQL_INSTANCE=${PROJECT_ID}:${REGION}:${APP}-db,\
509
+ _DB_SECRET_NAME=${APP}-db-password,\
510
+ _JWT_SECRET_NAME=${APP}-jwt-secret"
511
+ ```
512
+
513
+ ### cloudbuild.yaml structure
514
+
515
+ The `cloudbuild.yaml` uses substitution variables so the same file works for any project. The pipeline:
516
+
517
+ 1. **pull-backend-cache** — pulls `:latest` from Artifact Registry (cache for subsequent builds)
518
+ 2. **build-backend** — builds the Docker image from the app root with `--cache-from`
519
+ 3. **push-backend** — pushes `:COMMIT_SHA` and `:latest` tags
520
+ 4. **schema-sync** — creates/updates and executes the `<app>-migrate` Cloud Run Job (`DB_SYNCHRONIZE=true`)
521
+ 5. **deploy-backend** — deploys the Cloud Run service (`DB_SYNCHRONIZE=false`); runs after schema-sync
522
+ 6. **build-frontend** — installs app dependencies, runs `slingr sync-metadata`, builds the SPA (runs in parallel with schema-sync after push-backend)
523
+ 7. **deploy-frontend** — syncs `frontend/dist/` to GCS bucket
524
+
525
+ > **Steps 4 and 6 run in parallel** (`waitFor: [push-backend]`), reducing total pipeline time.
526
+
527
+ ---
528
+
529
+ ## Step 6 — Manual Deployment
530
+
531
+ Run from the **app root**:
532
+
533
+ ```bash
534
+ # Deploy HEAD
535
+ node ops/scripts/deploy.js
536
+
537
+ # Deploy a specific tag
538
+ node ops/scripts/deploy.js v1.2.3
539
+ ```
540
+
541
+ The script:
542
+
543
+ | Step | What happens |
544
+ |---|---|
545
+ | 0 | (Monorepo only) Builds framework + CLI from source; generates metadata (`slingr sync-metadata`); compiles backend + frontend |
546
+ | 1 | `docker build` with `--cache-from :latest` |
547
+ | 2 | `docker push` (`:SHA` and `:latest`) |
548
+ | 3 | Creates/updates and executes the `<app>-migrate` Cloud Run Job |
549
+ | 4 | Seeds default admin user via Cloud SQL Proxy (skipped if user already exists) |
550
+ | 5 | `gcloud run deploy <app>-backend` |
551
+ | 6 | `gsutil -m rsync -r -d frontend/dist gs://PROJECT_ID-<app>-frontend` |
552
+
553
+ ---
554
+
555
+ ## Step 7 — DNS & HTTPS Setup (Optional)
556
+
557
+ > **You need to own a domain before running this step.** Google does not provide a domain — you purchase one from a registrar (Namecheap, GoDaddy, Google/Squarespace Domains, etc.), typically for $10–20/year. Without a domain, the app continues to work over HTTP on the Load Balancer IP. HTTPS with a custom domain is an optional production step.
558
+
559
+ > **This step can be done any time after `setup-gcp.js`** — even weeks after the first deployment. The Load Balancer created in Step 3 is the only prerequisite.
560
+
561
+ By default, `setup-gcp.js` creates an HTTP (port 80) Load Balancer with an ephemeral IP. Run `setup-domain.js` to add a static IP, SSL certificate, and HTTPS:
562
+
563
+ Run from the **app root**:
564
+
565
+ ```bash
566
+ DOMAIN=app.yourdomain.com node ops/scripts/setup-domain.js
567
+
568
+ # Override project or region:
569
+ PROJECT_ID=my-project REGION=us-central1 DOMAIN=app.yourdomain.com \
570
+ node ops/scripts/setup-domain.js
571
+ ```
572
+
573
+ ### What setup-domain.js does
574
+
575
+ The script is idempotent — safe to re-run:
576
+
577
+ 1. **Reserves a static global IP** (`<app>-ip`) — replaces the ephemeral IP on the forwarding rule
578
+ 2. **Creates a Google-managed SSL certificate** (`<app>-cert`) for your domain — Google provisions and auto-renews it at no extra cost
579
+ 3. **Creates an HTTPS target proxy** (`<app>-https-proxy`) pointing at the existing URL map
580
+ 4. **Creates an HTTPS forwarding rule** (`<app>-https-rule`) on port 443 using the static IP
581
+ 5. **Prints the static IP** and the DNS record you need to configure
582
+
583
+ ### After running setup-domain.js
584
+
585
+ 1. **Configure DNS at your domain registrar:**
586
+ Add an `A` record pointing your domain to the static IP printed by the script.
587
+
588
+ ```
589
+ Type: A
590
+ Name: app (or @ for root domain)
591
+ Value: <static IP from script output>
592
+ TTL: 300
593
+ ```
594
+
595
+ 2. **Wait for certificate provisioning** — Google-managed certificates are provisioned automatically after DNS propagates. This takes **10–60 minutes**.
596
+
597
+ 3. **Verify the certificate is active:**
598
+ ```bash
599
+ gcloud compute ssl-certificates describe <app>-cert --global \
600
+ --format='value(managed.status, managed.domainStatus)'
601
+ ```
602
+ Status should be `ACTIVE`. If it shows `PROVISIONING`, wait and check again.
603
+
604
+ 4. **Test your HTTPS endpoint:**
605
+ ```bash
606
+ curl -I https://app.yourdomain.com/graphql
607
+ ```
608
+
609
+ ### Manual equivalent (no script)
610
+
611
+ If you prefer running the steps manually:
612
+
613
+ ```bash
614
+ # 1. Reserve a static IP
615
+ gcloud compute addresses create <app>-ip --global
616
+
617
+ # 2. Get the static IP
618
+ gcloud compute addresses describe <app>-ip --global --format='value(address)'
619
+
620
+ # 3. Create a Google-managed SSL certificate
621
+ gcloud compute ssl-certificates create <app>-cert \
622
+ --domains=app.yourdomain.com \
623
+ --global
624
+
625
+ # 4. Create HTTPS target proxy
626
+ gcloud compute target-https-proxies create <app>-https-proxy \
627
+ --url-map=<app>-url-map \
628
+ --ssl-certificates=<app>-cert \
629
+ --global
630
+
631
+ # 5. Create HTTPS forwarding rule
632
+ gcloud compute forwarding-rules create <app>-https-rule \
633
+ --load-balancing-scheme=EXTERNAL_MANAGED \
634
+ --global \
635
+ --target-https-proxy=<app>-https-proxy \
636
+ --ports=443 \
637
+ --address=<app>-ip
638
+ ```
639
+
640
+ ---
641
+
642
+ ## Networking
643
+
644
+ The networking layer is fully provisioned by `setup-gcp.js`. No manual network configuration is required after running that script.
645
+
646
+ ### What was set up
647
+
648
+ ```
649
+ Internet → Cloud HTTP(S) Load Balancer (global, external)
650
+
651
+ ├── URL map: /graphql, /auth/* → Backend Service
652
+ │ └── Serverless NEG → Cloud Run service
653
+
654
+ └── URL map: /** (default) → Backend Bucket
655
+ └── GCS Bucket (CDN-backed)
656
+ ```
657
+
658
+ | Component | Name | Purpose |
659
+ |---|---|---|
660
+ | Forwarding rule | `<app>-http-rule` (port 80) | Accepts incoming HTTP traffic |
661
+ | HTTP target proxy | `<app>-http-proxy` | Routes requests to the URL map |
662
+ | URL map | `<app>-url-map` | Path-based routing rules |
663
+ | Serverless NEG | `<app>-backend-neg` | Connects the LB to the Cloud Run service |
664
+ | Backend service | `<app>-backend-service` | Wraps the NEG for the URL map |
665
+ | Backend bucket | `<app>-frontend-bucket` | Wraps the GCS bucket with CDN |
666
+ | GCS bucket | `PROJECT_ID-<app>-frontend` | Serves the React SPA static files |
667
+
668
+ ### Cloud SQL connectivity
669
+
670
+ Cloud Run connects to Cloud SQL through the **Cloud SQL Auth Proxy** — a built-in sidecar that Google manages automatically. No VPC Connector, no firewall rules, and no open database ports are needed. The connection is:
671
+
672
+ - **Authenticated** via the backend service account IAM role (`roles/cloudsql.client`)
673
+ - **Encrypted** in transit automatically
674
+ - **Socket-based** inside the container: `DB_HOST=/cloudsql/PROJECT:REGION:INSTANCE`
675
+
676
+ ### Verifying the networking setup
677
+
678
+ ```bash
679
+ # Check the Load Balancer forwarding rules
680
+ gcloud compute forwarding-rules list --global
681
+
682
+ # Inspect the URL map routing rules
683
+ gcloud compute url-maps describe <app>-url-map --global
684
+
685
+ # Get the Load Balancer's public IP
686
+ gcloud compute forwarding-rules describe <app>-http-rule \
687
+ --global --format='value(IPAddress)'
688
+
689
+ # Check the serverless NEG
690
+ gcloud compute network-endpoint-groups describe <app>-backend-neg \
691
+ --region=REGION
692
+ ```
693
+
694
+ ---
695
+
696
+ ## Frontend Hosting
697
+
698
+ The React SPA is built with UmiJS (`npm run build` → `frontend/dist/`) and served from a GCS bucket behind the Cloud Load Balancer.
699
+
700
+ ### URL routing
701
+
702
+ | Path pattern | Destination |
703
+ |---|---|
704
+ | `/graphql` | Cloud Run backend (via serverless NEG) |
705
+ | `/auth/*` | Cloud Run backend (via serverless NEG) |
706
+ | `/**` (default) | GCS bucket CDN |
707
+
708
+ API calls use relative paths (`/graphql`, `/auth/...`). The LB routes them to the backend — no CORS issues, no hardcoded URLs.
709
+
710
+ ### Manual frontend deploy
711
+
712
+ Run from the **app root**:
713
+
714
+ ```bash
715
+ cd frontend
716
+ npm ci && npm run build
717
+
718
+ gsutil -m rsync -r -d dist gs://YOUR_PROJECT_ID-<app>-frontend
719
+ ```
720
+
721
+ ### CDN cache invalidation
722
+
723
+ After deploying new frontend files, force immediate CDN propagation:
724
+
725
+ ```bash
726
+ gcloud compute url-maps invalidate-cdn-cache <app>-url-map \
727
+ --path="/*" --global
728
+ ```
729
+
730
+ ---
731
+
732
+ ## Database Schema Sync
733
+
734
+ TypeORM's `synchronize: true` is **disabled in production** to prevent accidental table drops. Schema updates are handled by a dedicated Cloud Run Job before each deployment:
735
+
736
+ ```
737
+ Cloud Run Job: <app>-migrate
738
+ Image: same backend image, new tag
739
+ Env: DB_SYNCHRONIZE=true, NODE_ENV=migration
740
+ Runs once → exits after schema is in sync
741
+ → Production service deploys after job exits 0
742
+ ```
743
+
744
+ The job is created on first run and updated on every subsequent deploy. On the very first deployment to a fresh database, the schema-sync job creates all tables — no manual SQL needed.
745
+
746
+ ---
747
+
748
+ ## Default Admin User
749
+
750
+ `deploy.js` seeds a default admin user on the first deployment (idempotent — skipped if the user already exists). Configure the credentials in `deploy.js`:
751
+
752
+ ```bash
753
+ SEED_USER_EMAIL="sys@app.com"
754
+ SEED_USER_PASS="123456788"
755
+ ```
756
+
757
+ The seed runs locally via the Cloud SQL Auth Proxy, using the DB password fetched from Secret Manager. **Change the password immediately after first login.**
758
+
759
+ ---
760
+
761
+ ## Logging & Observability
762
+
763
+ Cloud Run services automatically ship all stdout/stderr to **Cloud Logging** — no configuration is required. Logs are available immediately after the first request.
764
+
765
+ ### View backend logs
766
+
767
+ ```bash
768
+ gcloud logging read \
769
+ 'resource.type="cloud_run_revision" AND resource.labels.service_name="<app>-backend"' \
770
+ --project=PROJECT_ID \
771
+ --limit=50 \
772
+ --format='table(timestamp, severity, textPayload)'
773
+ ```
774
+
775
+ ### Stream live logs
776
+
777
+ ```bash
778
+ gcloud beta run services logs tail <app>-backend --region=REGION
779
+ ```
780
+
781
+ ### Useful log filters
782
+
783
+ | What to find | Filter |
784
+ |---|---|
785
+ | All backend logs | `resource.type="cloud_run_revision" resource.labels.service_name="<app>-backend"` |
786
+ | Errors only | `resource.type="cloud_run_revision" resource.labels.service_name="<app>-backend" severity>=ERROR` |
787
+ | Schema sync job | `resource.type="cloud_run_job" resource.labels.job_name="<app>-migrate"` |
788
+ | DB connection errors | `resource.type="cloud_run_revision" textPayload:"connection refused" OR textPayload:"authentication failed"` |
789
+ | Specific request | `resource.type="cloud_run_revision" httpRequest.requestUrl:"/graphql"` |
790
+
791
+ Copy these filters into the [Cloud Console Logs Explorer](https://console.cloud.google.com/logs/query).
792
+
793
+ ### Cloud Monitoring — uptime check
794
+
795
+ Create an uptime check to monitor backend availability and receive alerts if the service goes down:
796
+
797
+ ```bash
798
+ gcloud monitoring uptime create \
799
+ --display-name="<app>-backend uptime" \
800
+ --resource-type=uptime-url \
801
+ --uri="http://<LB_IP>/graphql" \
802
+ --check-interval=300 \
803
+ --project=PROJECT_ID
804
+ ```
805
+
806
+ > Replace `<LB_IP>` with your Load Balancer IP (or custom domain if configured). Attach a notification channel in [Cloud Console → Monitoring → Alerting](https://console.cloud.google.com/monitoring/alerting).
807
+
808
+ ### Cloud Monitoring — error rate alert
809
+
810
+ Create a log-based metric and alert policy to notify when backend errors spike:
811
+
812
+ ```bash
813
+ # Create a log-based metric for backend errors
814
+ gcloud logging metrics create <app>-backend-errors \
815
+ --description="Count of ERROR+ logs from the backend" \
816
+ --log-filter='resource.type="cloud_run_revision" resource.labels.service_name="<app>-backend" severity>=ERROR' \
817
+ --project=PROJECT_ID
818
+
819
+ # Create an alert policy (fires if >5 errors in 5 minutes)
820
+ gcloud alpha monitoring policies create \
821
+ --policy='{
822
+ "displayName": "<app>-backend error spike",
823
+ "conditions": [{
824
+ "displayName": "Error log count",
825
+ "conditionThreshold": {
826
+ "filter": "metric.type=\"logging.googleapis.com/user/<app>-backend-errors\"",
827
+ "comparison": "COMPARISON_GT",
828
+ "thresholdValue": 5,
829
+ "duration": "300s",
830
+ "aggregations": [{"alignmentPeriod": "300s", "perSeriesAligner": "ALIGN_RATE"}]
831
+ }
832
+ }],
833
+ "alertStrategy": {"autoClose": "604800s"},
834
+ "combiner": "OR",
835
+ "enabled": true
836
+ }' \
837
+ --project=PROJECT_ID
838
+ ```
839
+
840
+ > Attach a notification channel (email, Slack, PagerDuty) in [Cloud Console → Monitoring → Alerting](https://console.cloud.google.com/monitoring/alerting) after creating the policy.
841
+
842
+ ---
843
+
844
+ ## IAM & Service Accounts Reference
845
+
846
+ ### Runtime service account (`<app>-backend`)
847
+
848
+ | Role | Purpose |
849
+ |---|---|
850
+ | `roles/cloudsql.client` | Connect to Cloud SQL via Auth Proxy at runtime |
851
+ | `roles/secretmanager.secretAccessor` | Read DB password and JWT secret at container startup |
852
+
853
+ ### Cloud Build service account (CI/CD)
854
+
855
+ | Role | Purpose |
856
+ |---|---|
857
+ | `roles/run.admin` | Deploy Cloud Run services and jobs |
858
+ | `roles/iam.serviceAccountUser` | Act as the backend SA when deploying Cloud Run |
859
+ | `roles/secretmanager.secretAccessor` | Read secrets referenced in `cloudbuild.yaml` |
860
+ | `roles/artifactregistry.writer` | Push Docker images to Artifact Registry |
861
+ | `roles/cloudsql.client` | Execute the schema-sync Cloud Run Job |
862
+ | `roles/storage.objectAdmin` | Sync frontend files to the GCS bucket |
863
+
864
+ ---
865
+
866
+ ## Ongoing Operations
867
+
868
+ ### Roll back to a previous revision
869
+
870
+ ```bash
871
+ # List recent revisions
872
+ gcloud run revisions list --service=<app>-backend --region=REGION
873
+
874
+ # Send 100% traffic to a previous revision
875
+ gcloud run services update-traffic <app>-backend \
876
+ --to-revisions=<app>-backend-00005-xyz=100 --region=REGION
877
+ ```
878
+
879
+ ### Scale to zero immediately
880
+
881
+ ```bash
882
+ gcloud run services update <app>-backend \
883
+ --min-instances=0 --region=REGION
884
+ ```
885
+
886
+ ### Connect to Cloud SQL from your local machine
887
+
888
+ ```bash
889
+ # Start proxy
890
+ cloud-sql-proxy PROJECT:REGION:<app>-db --port=5432 &
891
+
892
+ # Connect
893
+ psql -h 127.0.0.1 -U postgres -d <your_db_name>
894
+ ```
895
+
896
+ ---
897
+
898
+ ## Troubleshooting
899
+
900
+ ### Backend fails to connect to Cloud SQL
901
+
902
+ **Symptom:** `connect ENOENT /cloudsql/PROJECT:REGION:INSTANCE/.s.PGSQL.5433`
903
+
904
+ The socket file is always `.s.PGSQL.5432`. If the error shows `5433`, `DB_PORT` is wrong:
905
+
906
+ ```bash
907
+ gcloud run services update <app>-backend \
908
+ --region=REGION \
909
+ --update-env-vars=DB_PORT=5432 \
910
+ --project=PROJECT_ID
911
+ ```
912
+
913
+ **Other checks:**
914
+
915
+ 1. Verify `--add-cloudsql-instances` is set:
916
+ ```bash
917
+ gcloud run services describe <app>-backend --region=REGION \
918
+ --format='value(spec.template.metadata.annotations)'
919
+ ```
920
+ 2. Verify the backend SA has `roles/cloudsql.client`:
921
+ ```bash
922
+ gcloud projects get-iam-policy PROJECT_ID --flatten="bindings[].members" \
923
+ --filter="bindings.members:<app>-backend@*"
924
+ ```
925
+ 3. Check `DB_HOST` matches the instance connection name exactly: `PROJECT:REGION:INSTANCE`
926
+
927
+ ### Cloud Run service fails to start
928
+
929
+ **Symptom:** Deployment succeeds but health check fails → rollback
930
+
931
+ 1. Check startup logs: `gcloud beta run services logs tail <app>-backend --region=REGION`
932
+ 2. Verify all secrets exist: `gcloud secrets list --project=PROJECT_ID`
933
+
934
+ **Symptom:** `password authentication failed for user "postgres"`
935
+
936
+ The DB password and the Secret Manager secret are out of sync:
937
+
938
+ ```bash
939
+ NEW_PASS="$(openssl rand -base64 24)"
940
+
941
+ gcloud sql users set-password postgres \
942
+ --instance=<app>-db \
943
+ --password="$NEW_PASS" \
944
+ --project=PROJECT_ID
945
+
946
+ echo -n "$NEW_PASS" | gcloud secrets versions add <app>-db-password \
947
+ --data-file=- --project=PROJECT_ID
948
+ ```
949
+
950
+ Then redeploy.
951
+
952
+ ### Schema sync job exits non-zero
953
+
954
+ **Symptom:** `<app>-migrate` job execution fails
955
+
956
+ 1. Check job logs:
957
+ ```bash
958
+ gcloud logging read \
959
+ 'resource.type="cloud_run_job" resource.labels.job_name="<app>-migrate"' \
960
+ --limit=20 --format='value(textPayload)'
961
+ ```
962
+ 2. TypeORM exits 0 even when schema is already up to date — non-zero indicates a real error
963
+ 3. Verify `DB_HOST` socket path is correct in the job's env vars
964
+
965
+ ### Docker build fails: `cannot find module '@slingr/framework-backend'`
966
+
967
+ The Docker build context must be the **app root**, not `backend/`:
968
+
969
+ ```bash
970
+ # Run from your app root (the directory containing backend/ and frontend/)
971
+ docker build -f backend/Dockerfile .
972
+ # ^ app root
973
+ ```
974
+
975
+ ### Docker push fails: Unauthenticated request
976
+
977
+ **Symptom:** `Unauthenticated requests do not have permission "artifactregistry.repositories.uploadArtifacts"`
978
+
979
+ Configure `gcloud` as the Docker credential helper (runs once per machine):
980
+
981
+ ```bash
982
+ gcloud auth configure-docker us-central1-docker.pkg.dev
983
+ ```
984
+
985
+ If the error persists, ensure you are logged in with an account that has `roles/artifactregistry.writer`:
986
+
987
+ ```bash
988
+ gcloud auth login
989
+ gcloud auth configure-docker us-central1-docker.pkg.dev
990
+ ```
991
+
992
+ ### Cloud SQL Proxy is not recognized on local machine
993
+
994
+ **Symptom:** `"cloud-sql-proxy" is not recognized as an internal or external command`
995
+
996
+ Install the component and open a new terminal:
997
+
998
+ ```bash
999
+ gcloud components install cloud-sql-proxy
1000
+ cloud-sql-proxy --version
1001
+ ```
1002
+
1003
+ On Windows, restart your terminal (or VS Code) after install so `PATH` is refreshed.
1004
+
1005
+ ### Frontend not updating after deploy
1006
+
1007
+ **Symptom:** Old files served despite successful `gsutil rsync`
1008
+
1009
+ Force CDN cache invalidation:
1010
+
1011
+ ```bash
1012
+ gcloud compute url-maps invalidate-cdn-cache <app>-url-map \
1013
+ --path="/*" --global
1014
+ ```
1015
+
1016
+ ### Load Balancer returns 404 for frontend routes
1017
+
1018
+ **Symptom:** Navigating directly to `/dashboard` returns 404
1019
+
1020
+ The GCS bucket must have its 404 page set to `index.html`:
1021
+
1022
+ ```bash
1023
+ gcloud storage buckets describe gs://PROJECT_ID-<app>-frontend \
1024
+ --format='value(website)'
1025
+ ```
1026
+
1027
+ If missing:
1028
+
1029
+ ```bash
1030
+ gcloud storage buckets update gs://PROJECT_ID-<app>-frontend \
1031
+ --web-main-page-suffix=index.html \
1032
+ --web-error-page=index.html
1033
+ ```
1034
+
1035
+ ### SSL certificate stuck in PROVISIONING
1036
+
1037
+ **Symptom:** `gcloud compute ssl-certificates describe <app>-cert --global` shows `PROVISIONING` for more than 2 hours
1038
+
1039
+ 1. Verify the DNS A record points to the correct static IP:
1040
+ ```bash
1041
+ dig +short app.yourdomain.com
1042
+ gcloud compute addresses describe <app>-ip --global --format='value(address)'
1043
+ ```
1044
+ 2. Ensure the forwarding rules use the **static IP** (not the old ephemeral one). `setup-domain.js` handles this automatically.
1045
+ 3. Certificate provisioning requires the domain to resolve to the LB IP globally. Use [dnschecker.org](https://dnschecker.org) to verify propagation across regions.