forgecad 0.9.14 → 0.9.15

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 (219) hide show
  1. package/LICENSE +6 -4
  2. package/README.md +8 -4
  3. package/dist/assets/{AdminPage-eWGs2K6H.js → AdminPage-CDyGUinA.js} +2 -2
  4. package/dist/assets/{BenchmarkPage-CTrLKfpo.js → BenchmarkPage-DfPMY_-d.js} +4 -15
  5. package/dist/assets/{BlogPage-5nPesyds.js → BlogPage-kF0fkdJT.js} +2 -2
  6. package/dist/assets/{DocsPage-C4Y3nbYc.js → DocsPage-B954L3YN.js} +9 -3
  7. package/dist/assets/EditorApp-Beb-IZ0y.js +14014 -0
  8. package/dist/assets/{EditorApp-BAnckbsk.css → EditorApp-CuDLxKqL.css} +698 -0
  9. package/dist/assets/{EmbedViewer-C8fB4n5U.js → EmbedViewer-C77B-TrF.js} +3 -3
  10. package/dist/assets/{LandingPageProofDriven-jSz0LaMM.js → LandingPageProofDriven-Cr6fXMDj.js} +35 -37
  11. package/dist/assets/LegalPage-BRlScr9A.css +91 -0
  12. package/dist/assets/LegalPage-Dzklqmmg.js +39 -0
  13. package/dist/assets/{PricingPage-BMedqFef.css → PricingPage-BPF6HKyO.css} +25 -0
  14. package/dist/assets/{PricingPage-B83B90zh.js → PricingPage-zWXkvlwl.js} +19 -19
  15. package/dist/assets/{SettingsPage-DY889pcu.js → SettingsPage-Bz0of4KQ.js} +2 -2
  16. package/dist/assets/app-CE3sYcV7.css +3890 -0
  17. package/dist/assets/{app-bEww1ic4.js → app-D3kDkggg.js} +2293 -946
  18. package/dist/assets/cli/{render-Cho2uKG_.js → render-DSY3mMQa.js} +337 -7
  19. package/dist/assets/{constructionHistoryWorker-HYwzJY4m.js → constructionHistoryWorker-gpDo-uH2.js} +927 -243
  20. package/dist/assets/{evalWorker-CjQwJSE-.js → evalWorker-CU0Ke6DP.js} +7800 -4164
  21. package/dist/assets/{forgecad_geometry-CH2nvuLA.js → forgecad_geometry-Dgceylq9.js} +43 -1
  22. package/dist/assets/forgecad_geometry_bg-dD4RNQF1.wasm +0 -0
  23. package/dist/assets/{inspectWorker-DeRnMVv1.js → inspectWorker-COyp8XXA.js} +927 -243
  24. package/dist/assets/{javascript-70-4uGcz.js → javascript-1kQXfVaz.js} +1 -1
  25. package/dist/assets/landing-proof-driven-DiGqdtWa.js +18 -0
  26. package/dist/assets/{landing-proof-driven-oFYW6mjz.css → landing-proof-driven-ORyigZ6p.css} +13 -7
  27. package/dist/assets/legalContent-ZfFGMmi4.js +251 -0
  28. package/dist/assets/{manifold-CG9Fokx-.js → manifold-BRI5prcH.js} +1 -1
  29. package/dist/assets/{manifold-uRzgk5O8.js → manifold-C-3h2M7p.js} +2 -2
  30. package/dist/assets/{manifold-rmfAcdwF.js → manifold-DNkrUWpA.js} +1 -1
  31. package/dist/assets/{reportWorker-4cW_ZpoS.js → reportWorker-CdBz5bNg.js} +7538 -10857
  32. package/dist/assets/{scalar-sampling-budget-CfDiFvh7.js → scalar-sampling-budget-wJF98aY9.js} +6935 -4331
  33. package/dist/assets/{scanProxyWorker-Bs2TDgLw.js → scanProxyWorker-B-9VbLIs.js} +32 -1
  34. package/dist/assets/{solver-DuJAO8S6.js → solver-BZ9LPTHs.js} +1 -1
  35. package/dist/assets/solver_bg-DAHZJ_rw.wasm +0 -0
  36. package/dist/assets/{targets-D6PWsv6X.js → targets-B9sGB5nB.js} +1 -1
  37. package/dist/assets/{vendor-react-Da3A2QmU.js → vendor-react-6j1Kke-Y.js} +6 -5
  38. package/dist/cli/render.html +1 -1
  39. package/dist/docs/index.html +2 -2
  40. package/dist/docs-raw/AI/ai-native-cad.md +50 -0
  41. package/dist/docs-raw/AI/usage.md +3 -12
  42. package/dist/docs-raw/CLI.md +30 -10
  43. package/dist/docs-raw/component-model.md +27 -11
  44. package/dist/docs-raw/generated/assembly.md +301 -212
  45. package/dist/docs-raw/generated/concepts.md +235 -237
  46. package/dist/docs-raw/generated/core.md +283 -6
  47. package/dist/docs-raw/generated/curves.md +274 -361
  48. package/dist/docs-raw/generated/lib.md +7 -1
  49. package/dist/docs-raw/generated/output.md +19 -4
  50. package/dist/docs-raw/generated/runtime-names.md +41 -0
  51. package/dist/docs-raw/generated/sdf.md +31 -0
  52. package/dist/docs-raw/generated/sheet-metal.md +9 -0
  53. package/dist/docs-raw/generated/sketch.md +44 -1
  54. package/dist/docs-raw/generated/viewport.md +11 -3
  55. package/dist/docs-raw/guides/coordinate-system.md +20 -16
  56. package/dist/docs-raw/guides/geometry-conventions.md +2 -2
  57. package/dist/docs-raw/guides/inspection-bundles.md +2 -1
  58. package/dist/docs-raw/guides/joint-design.md +24 -0
  59. package/dist/docs-raw/guides/positioning.md +13 -3
  60. package/dist/docs-raw/legal/privacy.md +63 -0
  61. package/dist/docs-raw/legal/software-license.md +55 -0
  62. package/dist/docs-raw/legal/terms.md +87 -0
  63. package/dist/docs-raw/skills/forgecad-3d-reconstruction.md +1 -1
  64. package/dist/docs-raw/skills/forgecad-blockout-model.md +1 -1
  65. package/dist/docs-raw/skills/forgecad-component-model.md +11 -2
  66. package/dist/docs-raw/skills/forgecad-high-level-spec.md +1 -1
  67. package/dist/docs-raw/skills/forgecad-image-replicator.md +8 -8
  68. package/dist/docs-raw/skills/forgecad-lld.md +1 -1
  69. package/dist/docs-raw/skills/forgecad-make-a-model.md +1 -1
  70. package/dist/docs-raw/skills/forgecad-model-grader.md +2 -2
  71. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +2 -2
  72. package/dist/docs-raw/skills/forgecad-project.md +1 -1
  73. package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +1 -1
  74. package/dist/docs-raw/skills/forgecad-render-inspect.md +4 -2
  75. package/dist/docs-raw/skills/forgecad-visual-spec.md +1 -1
  76. package/dist/docs-raw/skills/forgecad.md +4 -3
  77. package/dist/index.html +40 -12
  78. package/dist/llms.txt +8 -0
  79. package/dist/site.webmanifest +1 -1
  80. package/dist/sitemap.xml +49 -13
  81. package/dist-cli/{check-compiler-U5SOPN7X.js → check-compiler-SDX5QIXI.js} +1 -2
  82. package/dist-cli/{check-query-propagation-XOKNSSYU.js → check-query-propagation-EAYEFT77.js} +1 -2
  83. package/dist-cli/{chunk-EXWGNL6K.js → chunk-N4O47JLF.js} +12540 -9046
  84. package/dist-cli/forgecad.js +1786 -679
  85. package/dist-cli/{forgecad_geometry-GYVNKPIE.js → forgecad_geometry-QOQIIP53.js} +42 -1
  86. package/dist-cli/forgecad_geometry_bg.wasm +0 -0
  87. package/dist-cli/{solver-46FFSK2U.js → solver-OK4HECRH.js} +0 -1
  88. package/dist-cli/solver_bg.wasm +0 -0
  89. package/dist-skill/CONTEXT.md +1117 -721
  90. package/dist-skill/SKILL.md +3 -2
  91. package/dist-skill/docs/API/core/concepts.md +64 -1
  92. package/dist-skill/docs/CLI.md +30 -10
  93. package/dist-skill/docs/generated/assembly.md +277 -229
  94. package/dist-skill/docs/generated/core.md +283 -6
  95. package/dist-skill/docs/generated/curves.md +272 -362
  96. package/dist-skill/docs/generated/lib.md +7 -1
  97. package/dist-skill/docs/generated/output.md +19 -4
  98. package/dist-skill/docs/generated/runtime-names.md +41 -0
  99. package/dist-skill/docs/generated/sdf.md +31 -0
  100. package/dist-skill/docs/generated/sheet-metal.md +9 -0
  101. package/dist-skill/docs/generated/sketch.md +44 -2
  102. package/dist-skill/docs/generated/viewport.md +2 -87
  103. package/dist-skill/docs/guides/coordinate-system.md +20 -16
  104. package/dist-skill/docs/guides/geometry-conventions.md +2 -2
  105. package/dist-skill/docs/guides/inspection-bundles.md +2 -1
  106. package/dist-skill/docs/guides/joint-design.md +24 -0
  107. package/dist-skill/docs/guides/positioning.md +13 -3
  108. package/dist-skill/library/forgecad-component-model/SKILL.md +10 -1
  109. package/dist-skill/library/forgecad-image-replicator/SKILL.md +6 -6
  110. package/dist-skill/library/forgecad-image-replicator/scripts/compare_images.py +166 -0
  111. package/dist-skill/library/forgecad-model-grader/SKILL.md +1 -1
  112. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +1 -1
  113. package/dist-skill/library/forgecad-render-inspect/SKILL.md +3 -1
  114. package/examples/api/assembly-kinematics-foundation.forge.js +65 -0
  115. package/examples/api/assembly-kinematics-four-bar.forge.js +115 -0
  116. package/examples/api/assembly-kinematics-limb.forge.js +116 -0
  117. package/examples/api/connector-frame-rig-chain.forge.js +102 -0
  118. package/examples/api/exact-sheet-shell-assembly.forge.js +0 -2
  119. package/examples/api/exact-surface-studio.forge.js +6 -8
  120. package/examples/api/helix-basics.forge.js +6 -6
  121. package/examples/api/lean-foundations/README.md +12 -0
  122. package/examples/api/lean-foundations/curve-blend-exact.forge.js +22 -0
  123. package/examples/api/lean-foundations/curve-fit-interpolation.forge.js +18 -0
  124. package/examples/api/lean-foundations/curve-helix-canonicalization.forge.js +27 -0
  125. package/examples/api/lean-foundations/curve-route-canonicalization.forge.js +16 -0
  126. package/examples/api/lean-foundations/curve-trim-reverse.forge.js +24 -0
  127. package/examples/api/lean-foundations/exact-curve-arc.forge.js +36 -0
  128. package/examples/api/mixed-edge-finishes-proof.forge.js +8 -11
  129. package/examples/api/route3d-elbow.forge.js +68 -0
  130. package/examples/api/transition-curves.forge.js +44 -15
  131. package/examples/api/y-blend-corner-showcase.forge.js +0 -2
  132. package/examples/generative/coral-vase.forge.js +1 -1
  133. package/examples/nurbs-tube.forge.js +1 -1
  134. package/package.json +14 -13
  135. package/dist/assets/EditorApp-lXv53A1m.js +0 -13610
  136. package/dist/assets/app-CsHnaBWt.css +0 -1789
  137. package/dist/assets/forgecad_geometry_bg-C5_E9Oa9.wasm +0 -0
  138. package/dist/assets/solver_bg-CWvv4lnN.wasm +0 -0
  139. package/dist/docs-raw/API/README.md +0 -16
  140. package/dist/docs-raw/API/core/concepts.md +0 -118
  141. package/dist/docs-raw/INDEX.md +0 -138
  142. package/dist/docs-raw/RELEASING.md +0 -87
  143. package/dist/docs-raw/agent-native-api.md +0 -27
  144. package/dist/docs-raw/beta-deployment.md +0 -304
  145. package/dist/docs-raw/beta-operations.md +0 -325
  146. package/dist/docs-raw/blueprint-first.md +0 -145
  147. package/dist/docs-raw/cli-monetization.md +0 -112
  148. package/dist/docs-raw/coding-best-practices.md +0 -120
  149. package/dist/docs-raw/coding.md +0 -340
  150. package/dist/docs-raw/deployment.md +0 -374
  151. package/dist/docs-raw/guides/skill-maintenance.md +0 -161
  152. package/dist/docs-raw/guides/surface-members.md +0 -82
  153. package/dist/docs-raw/harbor-cli.md +0 -854
  154. package/dist/docs-raw/internals/backend-vocabulary.md +0 -35
  155. package/dist/docs-raw/internals/compiler.md +0 -307
  156. package/dist/docs-raw/internals/constraint-solver-quality.md +0 -161
  157. package/dist/docs-raw/internals/constraint-solver.md +0 -176
  158. package/dist/docs-raw/internals/shape-from-slices.md +0 -152
  159. package/dist/docs-raw/internals/sketch-2d-pipeline.md +0 -108
  160. package/dist/docs-raw/platform/admin.md +0 -45
  161. package/dist/docs-raw/platform/architecture.md +0 -82
  162. package/dist/docs-raw/platform/auth.md +0 -139
  163. package/dist/docs-raw/platform/email.md +0 -67
  164. package/dist/docs-raw/platform/google-oauth-setup.md +0 -88
  165. package/dist/docs-raw/platform/observability.md +0 -197
  166. package/dist/docs-raw/platform/projects.md +0 -111
  167. package/dist/docs-raw/platform/sharing.md +0 -90
  168. package/dist/docs-raw/product/README.md +0 -39
  169. package/dist/docs-raw/product/api-as-product-language.md +0 -13
  170. package/dist/docs-raw/product/business-model.md +0 -15
  171. package/dist/docs-raw/product/competitive-positioning.md +0 -17
  172. package/dist/docs-raw/product/creative-manufacturing.md +0 -15
  173. package/dist/docs-raw/product/founder-story.md +0 -11
  174. package/dist/docs-raw/product/manufacturing-workflows.md +0 -15
  175. package/dist/docs-raw/product/onboarding-first-experience.md +0 -256
  176. package/dist/docs-raw/product/product-loop.md +0 -17
  177. package/dist/docs-raw/product/strategic-decisions.md +0 -22
  178. package/dist/docs-raw/product/user-outreach-email-templates.md +0 -161
  179. package/dist/docs-raw/product/user-segments.md +0 -15
  180. package/dist/docs-raw/product/vision.md +0 -26
  181. package/dist/docs-raw/rl-environments.md +0 -350
  182. package/dist/docs-raw/runbook.md +0 -611
  183. package/dist-cli/check-compiler-U5SOPN7X.js.map +0 -1
  184. package/dist-cli/check-query-propagation-XOKNSSYU.js.map +0 -1
  185. package/dist-cli/chunk-EXWGNL6K.js.map +0 -1
  186. package/dist-cli/forgecad.js.map +0 -1
  187. package/dist-cli/forgecad_geometry-GYVNKPIE.js.map +0 -1
  188. package/dist-cli/solver-46FFSK2U.js.map +0 -1
  189. package/dist-skill/SKILL-dev.md +0 -145
  190. package/dist-skill/docs-dev/API/core/concepts.md +0 -118
  191. package/dist-skill/docs-dev/CLI.md +0 -677
  192. package/dist-skill/docs-dev/agent-native-api.md +0 -27
  193. package/dist-skill/docs-dev/blueprint-first.md +0 -145
  194. package/dist-skill/docs-dev/coding-best-practices.md +0 -120
  195. package/dist-skill/docs-dev/coding.md +0 -340
  196. package/dist-skill/docs-dev/component-model.md +0 -164
  197. package/dist-skill/docs-dev/generated/assembly.md +0 -794
  198. package/dist-skill/docs-dev/generated/core.md +0 -2117
  199. package/dist-skill/docs-dev/generated/curves.md +0 -2583
  200. package/dist-skill/docs-dev/generated/lib.md +0 -169
  201. package/dist-skill/docs-dev/generated/output.md +0 -247
  202. package/dist-skill/docs-dev/generated/sdf.md +0 -446
  203. package/dist-skill/docs-dev/generated/sheet-metal.md +0 -504
  204. package/dist-skill/docs-dev/generated/sketch.md +0 -1811
  205. package/dist-skill/docs-dev/generated/viewport.md +0 -585
  206. package/dist-skill/docs-dev/generated/wood.md +0 -108
  207. package/dist-skill/docs-dev/guides/coordinate-system.md +0 -46
  208. package/dist-skill/docs-dev/guides/geometry-conventions.md +0 -52
  209. package/dist-skill/docs-dev/guides/inspection-bundles.md +0 -485
  210. package/dist-skill/docs-dev/guides/joint-design.md +0 -78
  211. package/dist-skill/docs-dev/guides/modeling-recipes.md +0 -78
  212. package/dist-skill/docs-dev/guides/positioning.md +0 -161
  213. package/dist-skill/docs-dev/guides/skill-maintenance.md +0 -161
  214. package/dist-skill/docs-dev/internals/backend-vocabulary.md +0 -35
  215. package/dist-skill/docs-dev/internals/compiler.md +0 -307
  216. package/dist-skill/docs-dev/internals/constraint-solver-quality.md +0 -161
  217. package/dist-skill/docs-dev/internals/constraint-solver.md +0 -176
  218. package/dist-skill/docs-dev/internals/sketch-2d-pipeline.md +0 -108
  219. package/dist-skill/library/forgecad-image-replicator/scripts/compare_images.mjs +0 -289
@@ -1,304 +0,0 @@
1
- # Beta Deployment — DNS, TLS, and Cloudflare Walkthrough
2
-
3
- This is the step-by-step guide for wiring `beta.forgecad.io` on the same Hetzner host as prod, end to end. Every term is defined on first use, and every definition defines the terms it depends on. If you already know a term, skim past it.
4
-
5
- **What you'll have at the end**: a separate `beta.forgecad.io` subdomain that points to the same server as prod, terminates TLS the same way, and routes to a separate container (set up later in another doc). Access will be gated via Cloudflare Access with an email allowlist.
6
-
7
- **What this doc doesn't cover**: writing `config/deploy.beta.yml`, the Postgres accessory, the scrub script, the CF Access policy itself. Those come after DNS+TLS is working. This doc is strictly the "make the URL resolve securely" layer.
8
-
9
- > Short answer: Cloudflare changes are only required when you want the shared
10
- > hosted beta URL on the real server. For a local browser smoke test of the
11
- > beta banner/admin flow, run `npm run beta:local` instead — no Cloudflare or
12
- > Hetzner changes involved.
13
-
14
- ---
15
-
16
- ## Concepts, defined once
17
-
18
- You'll see these terms throughout. Skip to the steps if you already know them.
19
-
20
- ### DNS (Domain Name System)
21
-
22
- The phone book of the internet. You type `forgecad.io` in a browser; DNS translates that name into an IP address like `65.108.210.215`. A **DNS record** is one entry in that phone book: "this name maps to this thing."
23
-
24
- ### DNS record types (only the ones you need here)
25
-
26
- - **A record**: "this name → this IPv4 address." Example: `forgecad.io → 65.108.210.215`.
27
- - **AAAA record**: Same thing for IPv6. Same server has one of these too, usually.
28
- - **CNAME record**: "this name is an alias for that other name." Example: `www.forgecad.io → forgecad.io`. When someone asks for `www`, DNS says "go look up `forgecad.io` instead." You can chain them.
29
- - **TXT record**: Arbitrary text attached to a name. Used for domain verification, email SPF/DKIM, etc. Not used here.
30
-
31
- ### Subdomain
32
-
33
- A name under a main domain. `beta.forgecad.io` is a subdomain of `forgecad.io`. You can add as many subdomains as you want; each gets its own DNS record.
34
-
35
- ### Cloudflare (CF)
36
-
37
- A service that sits in front of your server. DNS for `forgecad.io` is managed in Cloudflare's dashboard. Cloudflare can also proxy traffic — meaning the browser talks to Cloudflare, Cloudflare talks to your server, and Cloudflare handles things like TLS, caching, and WAF rules on the way through.
38
-
39
- ### "Proxied" vs "DNS only" in Cloudflare
40
-
41
- In the Cloudflare DNS dashboard, each record has a toggle called "Proxy status":
42
-
43
- - **DNS only** (grey cloud): Cloudflare returns your real server's IP to the browser. Browser talks directly to your server. Cloudflare does nothing else. Your server is exposed on the public internet with its real IP.
44
- - **Proxied** (orange cloud): Cloudflare returns one of its *own* IPs to the browser. Browser talks to Cloudflare, Cloudflare talks to your server. Cloudflare can do TLS, caching, WAF, and — crucially for us — **Cloudflare Access** (the email-allowlist gate) only works if the record is proxied.
45
-
46
- For `beta.forgecad.io`, you want **proxied**. Your prod `forgecad.io` record is already proxied — we'll match that.
47
-
48
- ### TLS (Transport Layer Security)
49
-
50
- The thing that turns `http://` into `https://`. Two jobs: (1) prove the server is really the server (the name on the certificate matches the domain), (2) encrypt the traffic between browser and server so middle-men can't read it.
51
-
52
- ### Certificate (TLS certificate, SSL certificate — same thing)
53
-
54
- A signed document that says "this server is legitimately `forgecad.io`, issued by [some trusted authority]." Browsers check it and either trust it (green padlock) or reject it (big scary warning).
55
-
56
- A certificate has:
57
- - A **subject** — the domain name it covers, e.g. `forgecad.io`.
58
- - A **SAN list** (Subject Alternative Names) — additional domain names the same certificate covers. `*.forgecad.io` in a SAN covers every single-level subdomain (`beta.forgecad.io`, `api.forgecad.io`, etc).
59
- - A **validity window** — from/to dates. If it's expired, browsers reject it.
60
- - A **signature** from the issuer.
61
-
62
- ### Certificate authority (CA)
63
-
64
- Whoever signs certificates. Browsers ship with a list of trusted CAs (Let's Encrypt, DigiCert, etc). Cloudflare acts as a CA for its "Origin Certificates" — see below.
65
-
66
- ### "Full Strict" mode (Cloudflare)
67
-
68
- Cloudflare supports several TLS modes between your server and its edge. You're on **Full Strict**, which means:
69
-
70
- - Browser → Cloudflare: TLS, verified by a normal public CA (Cloudflare handles this, you don't manage it).
71
- - Cloudflare → your Hetzner server: TLS, *and Cloudflare verifies the certificate on your server is valid*.
72
-
73
- "Valid" here is lenient — Cloudflare accepts both public CAs and its own Origin CA. It does **not** accept a self-signed cert in Full Strict mode. That's why you have a Cloudflare Origin Certificate on the Hetzner box.
74
-
75
- ### Origin server
76
-
77
- Your actual server. "Origin" is Cloudflare-speak for "the server behind me." In our case: the Hetzner box at `65.108.210.215`.
78
-
79
- ### Origin Certificate
80
-
81
- A TLS certificate issued by Cloudflare specifically for encrypting the Cloudflare-to-origin hop. Two important things:
82
-
83
- - It's **only** trusted by Cloudflare — regular browsers reject it. That's fine because browsers never see it directly; only Cloudflare's edge does.
84
- - Cloudflare will issue it for any domain you own, with any SAN list you request, with validity up to 15 years. You download it once, put it on the origin server, and forget.
85
-
86
- Today there's one on the Hetzner box covering `forgecad.io`. We need it to also cover `beta.forgecad.io`.
87
-
88
- ### SAN (Subject Alternative Name)
89
-
90
- The list of extra domain names a certificate covers beyond its primary subject. `*.forgecad.io` is a wildcard SAN — it matches any direct subdomain (`beta`, `api`, `docs`) but NOT double-nested ones (`beta.api.forgecad.io`).
91
-
92
- For this setup you want the new origin cert to have:
93
- - Subject: `forgecad.io`
94
- - SANs: `forgecad.io`, `*.forgecad.io`
95
-
96
- The wildcard means you never have to regenerate the cert when you add the next subdomain.
97
-
98
- ### kamal-proxy
99
-
100
- Kamal's built-in reverse proxy. Lives on the Hetzner server, listens on ports 80 and 443, terminates TLS using the origin certificate you provide, and routes requests to the right container based on the `Host:` header of the incoming request.
101
-
102
- So a request for `beta.forgecad.io` hits the kamal-proxy, which then knows "send this to the `forgecad-beta` service container" because that's what `config/deploy.beta.yml` will tell it.
103
-
104
- ### Reverse proxy
105
-
106
- Server-in-front-of-servers. The browser thinks it's talking to one thing (the proxy), but the proxy hands the request off to whichever backend service matches. "Reverse" because the proxy is in front of your *servers* (as opposed to a forward proxy, which is in front of *clients*).
107
-
108
- ### Host header
109
-
110
- Every HTTP request includes `Host: forgecad.io` in its headers. This is how one server can serve many domains — the kamal-proxy reads the `Host:` header and routes accordingly. `beta.forgecad.io` and `forgecad.io` both hit the same IP, but the proxy sends them to different containers based on that header.
111
-
112
- ### Cloudflare Access
113
-
114
- A separate Cloudflare product (in the "Zero Trust" dashboard) that adds an authentication gate in front of proxied domains. Before any request reaches your server, Cloudflare checks whether the user is on your allowlist. If not, they see a login prompt (Google, GitHub, email OTP, etc). If yes, the request proceeds.
115
-
116
- Free up to 50 users. Applied per hostname or URL pattern. For `beta.forgecad.io`, you set one policy: "allow these N email addresses; block everyone else."
117
-
118
- If you later want one Access app to cover several single-level envs, you can
119
- use a wildcard application such as `*.forgecad.io`; that matches
120
- `beta.forgecad.io` and `preview.forgecad.io`, but not `foo.beta.forgecad.io`.
121
-
122
- ### DNS TTL (Time To Live)
123
-
124
- Every DNS record has a TTL — how long resolvers are allowed to cache the answer before re-asking. Cloudflare's default is "Auto" (usually 5 min for proxied records). Matters if you typo the record: you'll be waiting up to TTL for the bad cached answer to expire. Low TTLs = faster propagation, slightly more DNS lookups. Cloudflare handles this well; leave it Auto.
125
-
126
- ---
127
-
128
- ## Step-by-step
129
-
130
- ### Step 1 — Add the DNS record
131
-
132
- **Goal**: `beta.forgecad.io` resolves to Cloudflare's proxy, which will forward to your Hetzner box.
133
-
134
- 1. Log in to Cloudflare dashboard → pick the `forgecad.io` zone → click "DNS" in the left sidebar.
135
- 2. Click "Add record".
136
- 3. Fill in:
137
- - **Type**: `A`
138
- - **Name**: `beta` (Cloudflare will show this as `beta.forgecad.io` — the root is implied)
139
- - **IPv4 address**: `65.108.210.215` (same as your existing `forgecad.io` A record)
140
- - **Proxy status**: toggle to the **orange cloud** (Proxied). If you leave it grey, Cloudflare Access won't work and TLS will go direct instead of through Cloudflare.
141
- - **TTL**: leave as Auto.
142
- 4. Click "Save".
143
-
144
- If you also have an AAAA (IPv6) record for prod, mirror it: same steps, type AAAA, same IPv6 address, orange cloud. If prod doesn't have AAAA, skip this — IPv4 alone is fine.
145
-
146
- **Verify** (from your local machine, a few seconds after saving):
147
-
148
- ```bash
149
- dig beta.forgecad.io +short
150
- ```
151
-
152
- You should see one of Cloudflare's IPs (something like `104.x.x.x` or `172.67.x.x`), not your Hetzner IP. That's correct — the orange cloud means Cloudflare returns its own IPs, then forwards to yours internally.
153
-
154
- If you see your Hetzner IP directly, the record is set to "DNS only" (grey cloud). Go back and toggle it orange.
155
-
156
- **Optional future-proofing.** If you expect more single-level same-server
157
- subdomains later (`preview.forgecad.io`, `demo.forgecad.io`, etc), you can
158
- also create one proxied wildcard `*` A record to the same Hetzner IP. Exact
159
- records still win, so `forgecad.io` and `beta` can stay explicit. The wildcard
160
- removes future DNS edits, but keep the Access note below in mind: wildcard
161
- Access apps only match the subdomain level you specify.
162
-
163
- ---
164
-
165
- ### Step 2 — Regenerate the origin certificate with the wildcard SAN
166
-
167
- **Goal**: replace the current origin cert on the Hetzner box with one that covers `*.forgecad.io` so `beta.forgecad.io` and any future subdomain "just work".
168
-
169
- Today, the origin cert in `.kamal/certs/` (and mirrored into `.kamal/secrets` as `CLOUDFLARE_ORIGIN_CERT` / `CLOUDFLARE_ORIGIN_KEY`) only lists `forgecad.io`. When Cloudflare tries to forward a request for `beta.forgecad.io` to the origin, it presents that hostname in the TLS handshake (via SNI — Server Name Indication), and the origin cert it gets back needs to match. Currently it won't, so you'd see a Cloudflare 526 error ("Invalid SSL certificate").
170
-
171
- #### Step 2a — Issue a new origin cert in Cloudflare
172
-
173
- 1. Cloudflare dashboard → pick `forgecad.io` zone → "SSL/TLS" section in the left sidebar → "Origin Server" tab.
174
- 2. Click "Create Certificate".
175
- 3. Fill in:
176
- - **Private key type**: "Let Cloudflare generate a private key and a CSR" (default; easiest).
177
- - **Hostnames**: pre-populated with `*.forgecad.io` and `forgecad.io`. Keep both. This is the SAN list on the resulting cert.
178
- - **Certificate Validity**: 15 years (max). Rotate it whenever you want; no reason to pick a shorter window.
179
- 4. Click "Create".
180
- 5. Cloudflare shows you the certificate (PEM format, starts with `-----BEGIN CERTIFICATE-----`) and the private key (starts with `-----BEGIN PRIVATE KEY-----`). **The private key is shown only this once.** Copy both into scratch files immediately.
181
-
182
- #### Step 2b — Install the new cert on the Hetzner box
183
-
184
- There are two paths — pick one based on how you manage the cert today.
185
-
186
- **Path A: the cert is stored in `.kamal/certs/` and referenced from `config/deploy.yml` or the kamal-proxy config**
187
-
188
- 1. On your laptop, replace the old files:
189
- ```bash
190
- cd /path/to/forgecad-private
191
- # back up first
192
- cp .kamal/certs/origin.pem .kamal/certs/origin.pem.bak
193
- cp .kamal/certs/origin.key .kamal/certs/origin.key.bak
194
- # paste the new cert + key from Cloudflare into these files
195
- ```
196
- (Exact filenames may differ in your repo — check `.kamal/certs/` and `config/deploy.yml` to confirm the names kamal-proxy expects.)
197
- 2. These files are gitignored (per the deployment doc). You'll copy them to the server manually in the next step.
198
-
199
- **Path B: the cert is stored in `.kamal/secrets` as env vars (`CLOUDFLARE_ORIGIN_CERT` / `CLOUDFLARE_ORIGIN_KEY`)**
200
-
201
- 1. Edit `.kamal/secrets` (it's a gitignored file you source before running Kamal commands):
202
- ```bash
203
- export CLOUDFLARE_ORIGIN_CERT="$(cat /tmp/new-origin.pem)"
204
- export CLOUDFLARE_ORIGIN_KEY="$(cat /tmp/new-origin.key)"
205
- ```
206
- Replace the old values entirely. The cert and key are both multi-line — if the file uses `""`-quoted heredocs or `$(cat file)`-style, keep the same format.
207
- 2. This is the most common setup for kamal-proxy. Confirm by grepping:
208
- ```bash
209
- grep -l CLOUDFLARE_ORIGIN config/deploy.yml .kamal/
210
- ```
211
-
212
- #### Step 2c — Deploy the cert change
213
-
214
- kamal-proxy reads the cert at startup. You need to restart it so it picks up the new one.
215
-
216
- ```bash
217
- # from the repo root, with .kamal/secrets sourced
218
- kamal proxy reboot
219
- ```
220
-
221
- This restarts the proxy container in-place. It's a few seconds of downtime — prod clients will see a connection reset and retry. If that's unacceptable, do it at a low-traffic time.
222
-
223
- #### Step 2d — Verify
224
-
225
- Three checks, from your laptop:
226
-
227
- 1. Prod still works (didn't break anything):
228
- ```bash
229
- curl -I https://forgecad.io
230
- ```
231
- Should return `200 OK` (or `301` for the marketing redirect) without any TLS error.
232
-
233
- 2. The cert now covers `*.forgecad.io`:
234
- ```bash
235
- openssl s_client -servername beta.forgecad.io -connect 65.108.210.215:443 < /dev/null 2>/dev/null | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"
236
- ```
237
- You should see a line listing both `forgecad.io` and `*.forgecad.io`. This bypasses Cloudflare and talks directly to your origin, verifying that the *origin* is presenting the right cert.
238
-
239
- 3. End-to-end through Cloudflare:
240
- ```bash
241
- curl -I https://beta.forgecad.io
242
- ```
243
- Right now this will return a Cloudflare error page (probably 521 "web server is down") because there's no container listening on the `beta.forgecad.io` host yet. **That's the expected result at this stage** — it proves TLS and DNS are working (no 526 "Invalid SSL certificate"), and the only thing missing is the beta container. That's the next doc.
244
-
245
- ---
246
-
247
- ### Step 3 — Set up Cloudflare Access (the email gate)
248
-
249
- **Goal**: only allowlisted emails can reach `beta.forgecad.io`; everyone else gets bounced at Cloudflare's edge before any request touches your server.
250
-
251
- 1. Cloudflare dashboard → click your profile icon (top-right) → go to "Zero Trust" (separate dashboard from the main one). First time only: Cloudflare will ask you to pick a team name — this is a URL slug for your auth pages (e.g. `forgecad` → `forgecad.cloudflareaccess.com`). Once. Picky.
252
- 2. In Zero Trust → "Access" → "Applications" → "Add an application" → "Self-hosted".
253
- 3. Fill in:
254
- - **Application name**: `ForgeCAD Beta`
255
- - **Session duration**: 24 hours (testers re-auth daily; bump if annoying).
256
- - **Application domain**: `beta.forgecad.io`
257
- 4. Click "Next".
258
- 5. On the policies page:
259
- - **Policy name**: `Beta testers`
260
- - **Action**: `Allow`
261
- - **Configure rules** → Include → "Emails" → add your email, plus any tester emails, one per line. You can also use "Emails ending in" for a domain match (e.g. `@forgecad.io`), but for a small tester list, explicit emails is clearer.
262
- 6. Under the same application, leave identity providers as the defaults — Cloudflare's one-time PIN (emails a code) is on by default, which is what you want for testers without Google/GitHub at your org. If you want them to log in with Google, enable the Google identity provider in Zero Trust → Settings → Authentication first.
263
- 7. Click "Next" through CORS / cookie / advanced settings (defaults are fine). Click "Add application".
264
-
265
- **Verify**:
266
-
267
- - Open an incognito window → `https://beta.forgecad.io`. You should see Cloudflare's login page, *not* a 521 error. Enter your email → get a one-time PIN → paste it → you'll then see the 521 (because no container yet). That 521 is the good one — it means you authenticated, and you're now talking to the (empty) origin.
268
- - Open another incognito window → try to access with a non-allowlisted email → it should refuse access with a "policy denied" message.
269
-
270
- ---
271
-
272
- ## Checkpoint — what you have now
273
-
274
- - `beta.forgecad.io` resolves to Cloudflare, proxied.
275
- - TLS works end-to-end: browser → Cloudflare (valid public cert) → Hetzner (valid origin cert covering `*.forgecad.io`).
276
- - Cloudflare Access enforces the email allowlist.
277
- - No container is listening yet, so every authenticated request gets a 521 from Cloudflare. That's the clean "ready for the app" state.
278
-
279
- ## What's next
280
-
281
- The remaining setup — writing `config/deploy.beta.yml`, the beta Postgres accessory, the scrub script, and the first manual `kamal deploy -c config/deploy.beta.yml` — doesn't touch DNS or TLS and is covered separately. Ask me to scaffold those when you've finished the steps above.
282
-
283
- ---
284
-
285
- ## Troubleshooting
286
-
287
- | Symptom | Likely cause | Fix |
288
- |---------|--------------|-----|
289
- | `dig beta.forgecad.io` returns your Hetzner IP, not a CF IP | DNS record is "DNS only" (grey cloud) | Toggle to Proxied (orange) |
290
- | `curl https://beta.forgecad.io` → 526 Invalid SSL | Origin cert doesn't cover `beta.forgecad.io` | Regenerate with `*.forgecad.io` SAN (Step 2) |
291
- | `curl https://beta.forgecad.io` → 521 Web server is down | No container listening on that Host | Expected until the beta app is deployed. If it persists after deploy, check kamal-proxy is routing `beta.forgecad.io` to the beta service. |
292
- | `curl https://beta.forgecad.io` → 1000 "DNS points to prohibited IP" | The DNS record points to a Cloudflare IP instead of the origin | You typo'd — an A record should have your Hetzner IP, not a Cloudflare IP |
293
- | Cloudflare login loop (keeps asking for PIN) | Session cookie domain mismatch | In Zero Trust → Access → your app → Settings, make sure "Application domain" is exactly `beta.forgecad.io`, not `https://beta.forgecad.io` |
294
- | TLS works but `*.forgecad.io` cert shows expired | Origin cert expired (15 years is the default, so unlikely, but possible if old) | Regenerate per Step 2 |
295
-
296
- ## Glossary recap
297
-
298
- - **Zone** — Cloudflare's word for a managed domain. `forgecad.io` is one zone.
299
- - **Edge** — Cloudflare's globally-distributed servers that the browser talks to when a record is proxied.
300
- - **Origin** — your actual server (Hetzner box).
301
- - **CN** (Common Name) — older term for the "primary" domain on a certificate, now largely superseded by SANs.
302
- - **Wildcard cert** — a cert whose SAN includes `*.forgecad.io`. Covers any direct subdomain.
303
- - **SNI** (Server Name Indication) — field in the TLS handshake where the client tells the server which hostname it wants a cert for. Lets one IP serve many TLS hostnames.
304
- - **521, 526** — Cloudflare-specific HTTP status codes. 521 = origin unreachable; 526 = origin TLS cert invalid; 522 = origin timeout. Useful because they tell you *where* in the chain the failure is.
@@ -1,325 +0,0 @@
1
- ---
2
- ---
3
-
4
- # ForgeCAD Beta — Operations Runbook
5
-
6
- The beta environment at [beta.forgecad.io](https://beta.forgecad.io) is the
7
- staging ground every change passes through before production. It runs on the
8
- same Hetzner VPS as prod, fully isolated at the process level.
9
-
10
- > **Isolation invariants.** Beta has its own Postgres container, its own
11
- > volume, its own JWT secret, its own file-storage volume, and its own
12
- > Kamal secrets file. A failure in beta — including a runaway migration,
13
- > a rogue query, or a filesystem fill-up — cannot affect prod.
14
-
15
- For the DNS, TLS and Cloudflare Access side of the setup, see
16
- [beta-deployment.md](beta-deployment.md). That document ends where this one
17
- starts — with `curl https://beta.forgecad.io` returning a 521 because no
18
- container is listening yet.
19
-
20
- ## Preferred admin commands
21
-
22
- Use the beta operator wrapper instead of memorizing raw `kamal` flags:
23
-
24
- ```bash
25
- npm run beta:setup # first-time setup
26
- npm run beta:deploy # routine deploy
27
- npm run beta:smoke # deploy, then open beta in the browser
28
- npm run beta:logs # tail logs
29
- npm run beta:refresh # prod -> beta scrubbed DB refresh (auto local/remote)
30
- npm run beta:db # interactive psql
31
- npm run beta:shell # app container shell
32
- npm run beta:local # local beta-like browser smoke test
33
- ```
34
-
35
- All of these are thin wrappers around `scripts/beta.sh` /
36
- `scripts/beta-local.sh`. They exist for operator ergonomics only; regular
37
- ForgeCAD users never see them in the public product CLI.
38
-
39
- Production has the matching `npm run prod:*` shortcuts in `scripts/prod.sh`
40
- for the same reason: muscle memory should be environment-prefixed, even though
41
- production is normally deployed by GitHub Actions on `mainline`.
42
-
43
- **Useful overrides**:
44
- - `FORGE_BETA_DEPLOY_CONFIG` — alternate Kamal config path
45
- - `FORGE_BETA_DESTINATION` — alternate Kamal destination name (default `beta`)
46
- - `FORGE_BETA_URL` — hosted beta URL to open after `beta:smoke`
47
- - `FORGE_BETA_SSH_HOST` — SSH target for remote refresh (default `hetzner`)
48
- - `FORGE_BETA_REFRESH_MODE=local|remote|auto` — force refresh behavior
49
-
50
- ---
51
-
52
- ## First-time setup
53
-
54
- Run this ONCE to stand beta up. After this, routine deploys are a single
55
- `npm run beta:deploy` (or `kamal deploy -d beta`).
56
-
57
- 1. **Finish the DNS + TLS + Cloudflare Access walkthrough**
58
- ([beta-deployment.md](beta-deployment.md)) — `curl https://beta.forgecad.io`
59
- should return 521, not NXDOMAIN / 526 / anything else. If it's not 521,
60
- stop and fix DNS/TLS before continuing.
61
-
62
- 2. **Populate `.kamal/secrets.beta`** from the template:
63
-
64
- ```bash
65
- cp .kamal/secrets.beta.example .kamal/secrets.beta
66
- # Edit .kamal/secrets.beta and replace every REPLACE_WITH_* placeholder.
67
- # Generate fresh values — do not reuse prod credentials.
68
- ```
69
-
70
- Every value MUST differ from `.kamal/secrets`. In particular:
71
- - `FORGE_JWT_SECRET` — regenerate with `openssl rand -base64 32`.
72
- - `POSTGRES_PASSWORD` — regenerate with `openssl rand -base64 24`.
73
- - `FORGE_DB_URL` — host is `forgecad-postgres-beta`, DB is `forgecad_beta`.
74
-
75
- 3. **Stand up beta with Kamal**:
76
-
77
- ```bash
78
- npm run beta:setup
79
- ```
80
-
81
- This builds the Docker image (remotely, on the VPS), pushes to GHCR,
82
- creates the `forgecad-postgres-beta` accessory with its own
83
- `kamal-postgresql-beta-data` volume, creates the `forgecad-project-data-beta`
84
- volume, starts the web container, registers `beta.forgecad.io` with
85
- kamal-proxy, and runs migrations.
86
-
87
- 4. **Verify**:
88
-
89
- ```bash
90
- curl -s https://beta.forgecad.io/api/health | jq
91
- # Expect: { "status": "ok", "environment": "beta", ... }
92
- ```
93
-
94
- Open `https://beta.forgecad.io` in a browser. You should see the orange
95
- **BETA** banner across the top on every route.
96
-
97
- 5. **Load real data** — see [Data refresh](#data-refresh) below.
98
-
99
- 6. **Grant yourself admin on beta** — by default the beta DB has no users
100
- until step 5 loads the prod dump. The scrub's admin allowlist at the top
101
- of `scripts/beta-scrub.sql` re-grants admin to listed emails.
102
-
103
- ---
104
-
105
- ## Routine deploys
106
-
107
- Beta deploys from whatever commit is about to go to prod next — typically
108
- `mainline` HEAD. There is no `beta` branch. Under the hood, the wrapper targets
109
- Kamal destination `beta`, so beta reads its own secrets file
110
- `.kamal/secrets.beta` rather than the prod `.kamal/secrets`.
111
-
112
- ```bash
113
- git checkout mainline && git pull
114
- npm run beta:deploy # rolling update, zero downtime
115
- npm run beta:smoke # same deploy, then open beta in the browser
116
- npm run beta:logs # tail logs
117
- ```
118
-
119
- Rollback:
120
-
121
- ```bash
122
- npm run beta:rollback
123
- ```
124
-
125
- No GitHub Actions are wired up for beta — it's manual, by design. If you
126
- want beta auto-deploy later, add a separate workflow file; don't piggy-back
127
- on the prod deploy workflow.
128
-
129
- ---
130
-
131
- ## Data refresh
132
-
133
- Beta should hold a scrubbed copy of prod data so the admin dashboard,
134
- quota logic, shared-link resolution and audit log all exercise realistic
135
- shapes. Refresh whenever that data is stale (typically monthly, or
136
- before a big feature test that depends on prod-like data).
137
-
138
- ```bash
139
- # Auto mode: runs locally if you're already on the Hetzner host, otherwise
140
- # streams the refresh + scrub payload over SSH.
141
- npm run beta:refresh
142
- ```
143
-
144
- Advanced/manual equivalents:
145
- - On the Hetzner host: `bash scripts/beta-refresh.sh`
146
- - From your laptop, forced remote mode: `FORGE_BETA_REFRESH_MODE=remote npm run beta:refresh`
147
-
148
- What the refresh does, step by step:
149
-
150
- 1. `pg_dump` the prod DB inside the `forgecad-postgres` container.
151
- 2. Stream the dump into the `forgecad-beta-postgres-beta` container.
152
- 3. `DROP DATABASE forgecad_beta WITH (FORCE)` + recreate.
153
- 4. `pg_restore` the dump into the fresh beta DB.
154
- 5. Run `scripts/beta-scrub.sql`:
155
- - Rewrites `users.email` to `user-<short>@beta.local` (non-deliverable).
156
- - Rewrites `users.name` to `Beta User <short>`.
157
- - Nulls `users.password_hash` and `users.avatar_url`.
158
- - Truncates `refresh_tokens`, `api_tokens`, `email_verification_tokens`,
159
- `password_reset_tokens`, `accounts`.
160
- - Nulls audit-log IP addresses, Stripe IDs on subscriptions.
161
- - Re-grants `admin` to the hardcoded allowlist at the top of the script.
162
-
163
- The scrub is idempotent and is guarded by a `current_database() NOT LIKE
164
- '%beta%'` check — it will refuse to run against prod even if you somehow
165
- aim it there.
166
-
167
- **Important identity behavior after refresh.** Beta copies prod users, then
168
- anonymizes them. Existing prod users do NOT keep their real email address on
169
- beta: the scrub rewrites imported emails to `user-<short>@beta.local`, clears
170
- password hashes, and deletes OAuth account links. That means signing in on beta
171
- with your real email after a refresh creates a separate beta-native account; it
172
- does not reconnect you to the imported prod-shaped account. That beta-native
173
- account is also ephemeral — the next refresh drops and recreates the beta DB
174
- from prod. This is expected today and is the main trade-off of the scrubbed
175
- shared-beta setup.
176
-
177
- **Adding a tester to the admin allowlist.** Edit the `beta_admin_allowlist`
178
- VALUES block near the top of `scripts/beta-scrub.sql`. The list uses the
179
- tester's PROD email (the scrub matches BEFORE rewriting emails). Re-run
180
- `scripts/beta-refresh.sh`, or just run the scrub again on its own:
181
-
182
- ```bash
183
- ssh hetzner 'docker exec -i forgecad-beta-postgres-beta \
184
- psql -U $(docker exec forgecad-beta-postgres-beta printenv POSTGRES_USER) \
185
- -d forgecad_beta -v ON_ERROR_STOP=1' \
186
- < scripts/beta-scrub.sql
187
- ```
188
-
189
- ---
190
-
191
- ## Accessing the beta DB
192
-
193
- ```bash
194
- # Interactive psql against beta
195
- npm run beta:db
196
-
197
- # One-shot query
198
- kamal accessory exec -d beta postgres-beta \
199
- "psql -U \$POSTGRES_USER -d forgecad_beta -c 'SELECT count(*) FROM users;'"
200
-
201
- # Shell into the beta web container
202
- npm run beta:shell
203
- ```
204
-
205
- ## Local smoke test
206
-
207
- If you just want to open a browser and sanity-check the beta banner, admin
208
- page, auth flows, and general feel without touching Cloudflare or the Hetzner
209
- host:
210
-
211
- ```bash
212
- npm run beta:local
213
- ```
214
-
215
- That starts the hosted frontend locally, runs the local Fastify server with
216
- `FORGE_ENVIRONMENT=beta`, opens the browser, and leaves prod/beta infra
217
- untouched.
218
-
219
- ---
220
-
221
- ## OAuth on beta
222
-
223
- Beta is **password-only** by default. The `GITHUB_CLIENT_ID`,
224
- `GITHUB_CLIENT_SECRET`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` env
225
- vars are intentionally not listed in `config/deploy.beta.yml`, so the
226
- server's OAuth handlers return unconfigured-provider errors.
227
-
228
- **Why not reuse prod OAuth apps?** Prod's OAuth apps have redirect URIs
229
- pinned to `https://forgecad.io/auth/callback/*`. Either we'd have to add
230
- `https://beta.forgecad.io/auth/callback/*` to the same app (which
231
- conflates beta and prod consent screens, and means a compromised beta
232
- secret can impersonate prod callbacks), or beta gets its own apps.
233
-
234
- **Trade-off of password-only**: testers can't click "Sign in with Google"
235
- on beta. They either (a) already have a password-login account on prod —
236
- which the scrub copies over with `password_hash = NULL`, so they'd need
237
- the operator to reset their password via the admin panel, or (b) register
238
- fresh on beta. That fresh beta-only account is separate from the imported
239
- scrubbed account and disappears on the next `npm run beta:refresh`. Low cost
240
- for the isolation win.
241
-
242
- **To enable OAuth on beta later**:
243
-
244
- 1. Create a **new** GitHub OAuth App. Set the callback URL to
245
- `https://beta.forgecad.io/auth/callback/github`. Do NOT add this
246
- callback to the prod app.
247
- 2. Create a **new** Google OAuth client (OAuth 2.0 client ID, web app).
248
- Authorised redirect URI `https://beta.forgecad.io/auth/callback/google`.
249
-
250
- Use Google Auth Platform → Clients in the Google Cloud Console for this.
251
- Do not use `gcloud iam oauth-clients`; that command creates IAM/IAP OAuth
252
- application resources, not the normal Google Sign-In web client that
253
- ForgeCAD's `/auth/callback/google` flow expects.
254
- 3. Add the four secrets to `.kamal/secrets.beta`:
255
-
256
- ```bash
257
- GITHUB_CLIENT_ID=...
258
- GITHUB_CLIENT_SECRET=...
259
- GOOGLE_CLIENT_ID=...
260
- GOOGLE_CLIENT_SECRET=...
261
- ```
262
- 4. Add them to `env.secret:` in `config/deploy.beta.yml`.
263
- 5. `kamal deploy -d beta`.
264
-
265
- ---
266
-
267
- ## Adding a new tester
268
-
269
- Access to `beta.forgecad.io` is gated by Cloudflare Access (email
270
- allowlist). Adding a tester is a two-step process:
271
-
272
- 1. Add their email to the Cloudflare Access policy. See the "Adding a
273
- tester" section of [beta-deployment.md](beta-deployment.md) — that
274
- doc owns the CF Access console walkthrough.
275
- 2. If they need admin rights on beta (i.e. access to `/admin`):
276
- - Add their prod email to the `beta_admin_allowlist` block in
277
- `scripts/beta-scrub.sql`, OR
278
- - Run `UPDATE users SET role = 'admin' WHERE email = 'user-<short>@beta.local';`
279
- directly (non-persistent — gets reset on the next data refresh).
280
-
281
- ---
282
-
283
- ## Adding beta compute later
284
-
285
- The geometry compute backend (`Dockerfile.backend`) is NOT deployed to beta
286
- initially. `FORGE_COMPUTE_URL=http://unavailable.local:9999` in
287
- `config/deploy.beta.yml` means Pro compute requests on beta fail fast
288
- rather than silently routing through to prod's backend container.
289
-
290
- When a test case genuinely needs the compute path on beta:
291
-
292
- 1. Create `config/deploy.backend.beta.yml`, cloned from
293
- `config/deploy.backend.yml`:
294
- - `service: forgecad-backend-beta`
295
- - `network-alias: forgecad-backend-beta`
296
- - `host: backend-beta.internal` (internal only, not Cloudflare-routed)
297
- 2. Update `config/deploy.beta.yml` env:
298
- `FORGE_COMPUTE_URL: http://forgecad-backend-beta:4510`.
299
- 3. `kamal deploy -c config/deploy.backend.beta.yml`, then
300
- `kamal deploy -d beta`.
301
-
302
- Keep the backend deploy config symmetrical with its web counterpart —
303
- separate service name, separate network alias, separate secrets.
304
-
305
- ---
306
-
307
- ## Debugging connectivity
308
-
309
- | Symptom | Likely cause |
310
- |-----------------------------------------------|-----------------------------------------------------|
311
- | `curl beta.forgecad.io` → `NXDOMAIN` | DNS not propagated yet; see [beta-deployment.md](beta-deployment.md) |
312
- | `curl beta.forgecad.io` → 521 | Web container not running — `kamal deploy -d beta` |
313
- | `curl beta.forgecad.io` → 526 | Origin cert missing `beta.forgecad.io` SAN; see [beta-deployment.md](beta-deployment.md) § "Regenerating the origin cert" |
314
- | 502 / `no server available` | kamal-proxy has the route but the container is failing healthchecks. `kamal app logs -d beta`. |
315
- | App loads but no BETA banner | `FORGE_ENVIRONMENT` missing from the container env. Check `kamal app exec -d beta "env \| grep FORGE_ENVIRONMENT"`. |
316
- | BETA banner shown on `forgecad.io` (prod) | **Stop and investigate immediately** — this means prod is running beta env vars. Check `kamal app exec "env \| grep FORGE_ENVIRONMENT"` on prod. |
317
- | Admin page shows `environment: production` on beta | Same as above — env var mis-set. |
318
-
319
- ---
320
-
321
- ## Related docs
322
-
323
- - [beta-deployment.md](beta-deployment.md) — DNS, TLS, Cloudflare Access.
324
- - [deployment.md](deployment.md) — overall deployment architecture.
325
- - [runbook.md](runbook.md) — day-to-day operations (prod).