gitspace 0.2.0-rc.1

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 (318) hide show
  1. package/.claude/settings.local.json +21 -0
  2. package/.gitspace/bundle.json +50 -0
  3. package/.gitspace/select/01-status.sh +40 -0
  4. package/.gitspace/setup/01-install-deps.sh +12 -0
  5. package/.gitspace/setup/02-typecheck.sh +16 -0
  6. package/AGENTS.md +439 -0
  7. package/CLAUDE.md +1 -0
  8. package/LICENSE +25 -0
  9. package/README.md +607 -0
  10. package/bin/gssh +62 -0
  11. package/bun.lock +647 -0
  12. package/docs/CONNECTION.md +623 -0
  13. package/docs/GATEWAY-WORKER.md +319 -0
  14. package/docs/GETTING-STARTED.md +448 -0
  15. package/docs/GITSPACE-PLATFORM.md +1819 -0
  16. package/docs/INFRASTRUCTURE.md +1347 -0
  17. package/docs/PROTOCOL.md +619 -0
  18. package/docs/QUICKSTART.md +174 -0
  19. package/docs/RELAY.md +327 -0
  20. package/docs/REMOTE-DESIGN.md +549 -0
  21. package/docs/ROADMAP.md +564 -0
  22. package/docs/SITE_DOCS_FIGMA_MAKE.md +1167 -0
  23. package/docs/STACK-DESIGN.md +588 -0
  24. package/docs/UNIFIED_ARCHITECTURE.md +292 -0
  25. package/experiments/pty-benchmark.ts +148 -0
  26. package/experiments/pty-latency.ts +100 -0
  27. package/experiments/router/client.ts +199 -0
  28. package/experiments/router/protocol.ts +74 -0
  29. package/experiments/router/router.ts +217 -0
  30. package/experiments/router/session.ts +180 -0
  31. package/experiments/router/test.ts +133 -0
  32. package/experiments/socket-bandwidth.ts +77 -0
  33. package/homebrew/gitspace.rb +45 -0
  34. package/landing-page/ATTRIBUTIONS.md +3 -0
  35. package/landing-page/README.md +11 -0
  36. package/landing-page/bun.lock +801 -0
  37. package/landing-page/guidelines/Guidelines.md +61 -0
  38. package/landing-page/index.html +37 -0
  39. package/landing-page/package.json +90 -0
  40. package/landing-page/postcss.config.mjs +15 -0
  41. package/landing-page/public/_redirects +1 -0
  42. package/landing-page/public/favicon.png +0 -0
  43. package/landing-page/src/app/App.tsx +53 -0
  44. package/landing-page/src/app/components/figma/ImageWithFallback.tsx +27 -0
  45. package/landing-page/src/app/components/ui/accordion.tsx +66 -0
  46. package/landing-page/src/app/components/ui/alert-dialog.tsx +157 -0
  47. package/landing-page/src/app/components/ui/alert.tsx +66 -0
  48. package/landing-page/src/app/components/ui/aspect-ratio.tsx +11 -0
  49. package/landing-page/src/app/components/ui/avatar.tsx +53 -0
  50. package/landing-page/src/app/components/ui/badge.tsx +46 -0
  51. package/landing-page/src/app/components/ui/breadcrumb.tsx +109 -0
  52. package/landing-page/src/app/components/ui/button.tsx +57 -0
  53. package/landing-page/src/app/components/ui/calendar.tsx +75 -0
  54. package/landing-page/src/app/components/ui/card.tsx +92 -0
  55. package/landing-page/src/app/components/ui/carousel.tsx +241 -0
  56. package/landing-page/src/app/components/ui/chart.tsx +353 -0
  57. package/landing-page/src/app/components/ui/checkbox.tsx +32 -0
  58. package/landing-page/src/app/components/ui/collapsible.tsx +33 -0
  59. package/landing-page/src/app/components/ui/command.tsx +177 -0
  60. package/landing-page/src/app/components/ui/context-menu.tsx +252 -0
  61. package/landing-page/src/app/components/ui/dialog.tsx +135 -0
  62. package/landing-page/src/app/components/ui/drawer.tsx +132 -0
  63. package/landing-page/src/app/components/ui/dropdown-menu.tsx +257 -0
  64. package/landing-page/src/app/components/ui/form.tsx +168 -0
  65. package/landing-page/src/app/components/ui/hover-card.tsx +44 -0
  66. package/landing-page/src/app/components/ui/input-otp.tsx +77 -0
  67. package/landing-page/src/app/components/ui/input.tsx +21 -0
  68. package/landing-page/src/app/components/ui/label.tsx +24 -0
  69. package/landing-page/src/app/components/ui/menubar.tsx +276 -0
  70. package/landing-page/src/app/components/ui/navigation-menu.tsx +168 -0
  71. package/landing-page/src/app/components/ui/pagination.tsx +127 -0
  72. package/landing-page/src/app/components/ui/popover.tsx +48 -0
  73. package/landing-page/src/app/components/ui/progress.tsx +31 -0
  74. package/landing-page/src/app/components/ui/radio-group.tsx +45 -0
  75. package/landing-page/src/app/components/ui/resizable.tsx +56 -0
  76. package/landing-page/src/app/components/ui/scroll-area.tsx +58 -0
  77. package/landing-page/src/app/components/ui/select.tsx +189 -0
  78. package/landing-page/src/app/components/ui/separator.tsx +28 -0
  79. package/landing-page/src/app/components/ui/sheet.tsx +139 -0
  80. package/landing-page/src/app/components/ui/sidebar.tsx +726 -0
  81. package/landing-page/src/app/components/ui/skeleton.tsx +13 -0
  82. package/landing-page/src/app/components/ui/slider.tsx +63 -0
  83. package/landing-page/src/app/components/ui/sonner.tsx +25 -0
  84. package/landing-page/src/app/components/ui/switch.tsx +31 -0
  85. package/landing-page/src/app/components/ui/table.tsx +116 -0
  86. package/landing-page/src/app/components/ui/tabs.tsx +66 -0
  87. package/landing-page/src/app/components/ui/textarea.tsx +18 -0
  88. package/landing-page/src/app/components/ui/toggle-group.tsx +73 -0
  89. package/landing-page/src/app/components/ui/toggle.tsx +47 -0
  90. package/landing-page/src/app/components/ui/tooltip.tsx +61 -0
  91. package/landing-page/src/app/components/ui/use-mobile.ts +21 -0
  92. package/landing-page/src/app/components/ui/utils.ts +6 -0
  93. package/landing-page/src/components/docs/DocsContent.tsx +718 -0
  94. package/landing-page/src/components/docs/DocsSidebar.tsx +84 -0
  95. package/landing-page/src/components/landing/CTA.tsx +59 -0
  96. package/landing-page/src/components/landing/Comparison.tsx +84 -0
  97. package/landing-page/src/components/landing/FaultyTerminal.tsx +424 -0
  98. package/landing-page/src/components/landing/Features.tsx +201 -0
  99. package/landing-page/src/components/landing/Hero.tsx +142 -0
  100. package/landing-page/src/components/landing/Pricing.tsx +140 -0
  101. package/landing-page/src/components/landing/Roadmap.tsx +86 -0
  102. package/landing-page/src/components/landing/Security.tsx +81 -0
  103. package/landing-page/src/components/landing/TerminalWindow.tsx +27 -0
  104. package/landing-page/src/components/landing/UseCases.tsx +55 -0
  105. package/landing-page/src/components/landing/Workflow.tsx +101 -0
  106. package/landing-page/src/components/layout/DashboardNavbar.tsx +37 -0
  107. package/landing-page/src/components/layout/Footer.tsx +55 -0
  108. package/landing-page/src/components/layout/LandingNavbar.tsx +82 -0
  109. package/landing-page/src/components/ui/badge.tsx +39 -0
  110. package/landing-page/src/components/ui/breadcrumb.tsx +115 -0
  111. package/landing-page/src/components/ui/button.tsx +57 -0
  112. package/landing-page/src/components/ui/card.tsx +79 -0
  113. package/landing-page/src/components/ui/mock-terminal.tsx +68 -0
  114. package/landing-page/src/components/ui/separator.tsx +28 -0
  115. package/landing-page/src/lib/utils.ts +6 -0
  116. package/landing-page/src/main.tsx +10 -0
  117. package/landing-page/src/pages/Dashboard.tsx +133 -0
  118. package/landing-page/src/pages/DocsPage.tsx +79 -0
  119. package/landing-page/src/pages/LandingPage.tsx +31 -0
  120. package/landing-page/src/pages/TerminalView.tsx +106 -0
  121. package/landing-page/src/styles/fonts.css +0 -0
  122. package/landing-page/src/styles/index.css +3 -0
  123. package/landing-page/src/styles/tailwind.css +4 -0
  124. package/landing-page/src/styles/theme.css +181 -0
  125. package/landing-page/vite.config.ts +19 -0
  126. package/npm/darwin-arm64/bin/gssh +0 -0
  127. package/npm/darwin-arm64/package.json +20 -0
  128. package/package.json +74 -0
  129. package/scripts/build.ts +284 -0
  130. package/scripts/release.ts +140 -0
  131. package/src/__tests__/test-utils.ts +298 -0
  132. package/src/commands/__tests__/serve-messages.test.ts +190 -0
  133. package/src/commands/access.ts +298 -0
  134. package/src/commands/add.ts +452 -0
  135. package/src/commands/auth.ts +364 -0
  136. package/src/commands/connect.ts +287 -0
  137. package/src/commands/directory.ts +16 -0
  138. package/src/commands/host.ts +396 -0
  139. package/src/commands/identity.ts +184 -0
  140. package/src/commands/list.ts +200 -0
  141. package/src/commands/relay.ts +315 -0
  142. package/src/commands/remove.ts +241 -0
  143. package/src/commands/serve.ts +1493 -0
  144. package/src/commands/share.ts +456 -0
  145. package/src/commands/status.ts +125 -0
  146. package/src/commands/switch.ts +353 -0
  147. package/src/commands/tmux.ts +317 -0
  148. package/src/core/__tests__/access.test.ts +240 -0
  149. package/src/core/access.ts +277 -0
  150. package/src/core/bundle.ts +342 -0
  151. package/src/core/config.ts +510 -0
  152. package/src/core/git.ts +317 -0
  153. package/src/core/github.ts +151 -0
  154. package/src/core/identity.ts +631 -0
  155. package/src/core/linear.ts +225 -0
  156. package/src/core/shell.ts +161 -0
  157. package/src/core/trusted-relays.ts +315 -0
  158. package/src/index.ts +821 -0
  159. package/src/lib/remote-session/index.ts +7 -0
  160. package/src/lib/remote-session/protocol.ts +267 -0
  161. package/src/lib/remote-session/session-handler.ts +581 -0
  162. package/src/lib/remote-session/workspace-scanner.ts +167 -0
  163. package/src/lib/tmux-lite/README.md +81 -0
  164. package/src/lib/tmux-lite/cli.ts +796 -0
  165. package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +349 -0
  166. package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +291 -0
  167. package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +142 -0
  168. package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +339 -0
  169. package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +477 -0
  170. package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +499 -0
  171. package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +371 -0
  172. package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +573 -0
  173. package/src/lib/tmux-lite/crypto/access-control.test.ts +512 -0
  174. package/src/lib/tmux-lite/crypto/access-control.ts +320 -0
  175. package/src/lib/tmux-lite/crypto/frames.test.ts +262 -0
  176. package/src/lib/tmux-lite/crypto/frames.ts +141 -0
  177. package/src/lib/tmux-lite/crypto/handshake.ts +894 -0
  178. package/src/lib/tmux-lite/crypto/identity.test.ts +220 -0
  179. package/src/lib/tmux-lite/crypto/identity.ts +286 -0
  180. package/src/lib/tmux-lite/crypto/index.ts +51 -0
  181. package/src/lib/tmux-lite/crypto/invites.test.ts +381 -0
  182. package/src/lib/tmux-lite/crypto/invites.ts +215 -0
  183. package/src/lib/tmux-lite/crypto/keyexchange.ts +435 -0
  184. package/src/lib/tmux-lite/crypto/keys.test.ts +58 -0
  185. package/src/lib/tmux-lite/crypto/keys.ts +47 -0
  186. package/src/lib/tmux-lite/crypto/secretbox.test.ts +169 -0
  187. package/src/lib/tmux-lite/crypto/secretbox.ts +124 -0
  188. package/src/lib/tmux-lite/handshake-handler.ts +451 -0
  189. package/src/lib/tmux-lite/protocol.test.ts +307 -0
  190. package/src/lib/tmux-lite/protocol.ts +266 -0
  191. package/src/lib/tmux-lite/relay-client.ts +506 -0
  192. package/src/lib/tmux-lite/server.ts +1250 -0
  193. package/src/lib/tmux-lite/shell-integration.sh +37 -0
  194. package/src/lib/tmux-lite/terminal-queries.test.ts +54 -0
  195. package/src/lib/tmux-lite/terminal-queries.ts +49 -0
  196. package/src/relay/__tests__/e2e-flow.test.ts +1284 -0
  197. package/src/relay/__tests__/helpers/auth.ts +354 -0
  198. package/src/relay/__tests__/helpers/ports.ts +51 -0
  199. package/src/relay/__tests__/protocol-validation.test.ts +265 -0
  200. package/src/relay/authorization.ts +303 -0
  201. package/src/relay/embedded-assets.generated.d.ts +15 -0
  202. package/src/relay/identity.ts +352 -0
  203. package/src/relay/index.ts +57 -0
  204. package/src/relay/pipes.test.ts +427 -0
  205. package/src/relay/pipes.ts +195 -0
  206. package/src/relay/protocol.ts +804 -0
  207. package/src/relay/registries.test.ts +437 -0
  208. package/src/relay/registries.ts +593 -0
  209. package/src/relay/server.test.ts +1323 -0
  210. package/src/relay/server.ts +1092 -0
  211. package/src/relay/signing.ts +238 -0
  212. package/src/relay/types.ts +69 -0
  213. package/src/serve/client-session-manager.ts +622 -0
  214. package/src/serve/daemon.ts +497 -0
  215. package/src/serve/pty-session.ts +236 -0
  216. package/src/serve/types.ts +169 -0
  217. package/src/shared/components/Flow.tsx +453 -0
  218. package/src/shared/components/Flow.tui.tsx +343 -0
  219. package/src/shared/components/Flow.web.tsx +442 -0
  220. package/src/shared/components/Inbox.tsx +446 -0
  221. package/src/shared/components/Inbox.tui.tsx +262 -0
  222. package/src/shared/components/Inbox.web.tsx +329 -0
  223. package/src/shared/components/MachineList.tsx +187 -0
  224. package/src/shared/components/MachineList.tui.tsx +161 -0
  225. package/src/shared/components/MachineList.web.tsx +210 -0
  226. package/src/shared/components/ProjectList.tsx +176 -0
  227. package/src/shared/components/ProjectList.tui.tsx +109 -0
  228. package/src/shared/components/ProjectList.web.tsx +143 -0
  229. package/src/shared/components/SpacesBrowser.tsx +332 -0
  230. package/src/shared/components/SpacesBrowser.tui.tsx +163 -0
  231. package/src/shared/components/SpacesBrowser.web.tsx +221 -0
  232. package/src/shared/components/index.ts +103 -0
  233. package/src/shared/hooks/index.ts +16 -0
  234. package/src/shared/hooks/useNavigation.ts +226 -0
  235. package/src/shared/index.ts +122 -0
  236. package/src/shared/providers/LocalMachineProvider.ts +425 -0
  237. package/src/shared/providers/MachineProvider.ts +165 -0
  238. package/src/shared/providers/RemoteMachineProvider.ts +444 -0
  239. package/src/shared/providers/index.ts +26 -0
  240. package/src/shared/types.ts +145 -0
  241. package/src/tui/adapters.ts +120 -0
  242. package/src/tui/app.tsx +1816 -0
  243. package/src/tui/components/Terminal.tsx +580 -0
  244. package/src/tui/hooks/index.ts +35 -0
  245. package/src/tui/hooks/useAppState.ts +314 -0
  246. package/src/tui/hooks/useDaemonStatus.ts +174 -0
  247. package/src/tui/hooks/useInboxTUI.ts +113 -0
  248. package/src/tui/hooks/useRemoteMachines.ts +209 -0
  249. package/src/tui/index.ts +24 -0
  250. package/src/tui/state.ts +299 -0
  251. package/src/tui/terminal-bracketed-paste.test.ts +45 -0
  252. package/src/tui/terminal-bracketed-paste.ts +47 -0
  253. package/src/types/bundle.ts +112 -0
  254. package/src/types/config.ts +89 -0
  255. package/src/types/errors.ts +206 -0
  256. package/src/types/identity.ts +284 -0
  257. package/src/types/workspace-fuzzy.ts +49 -0
  258. package/src/types/workspace.ts +151 -0
  259. package/src/utils/bun-socket-writer.ts +80 -0
  260. package/src/utils/deps.ts +127 -0
  261. package/src/utils/fuzzy-match.ts +125 -0
  262. package/src/utils/logger.ts +127 -0
  263. package/src/utils/markdown.ts +254 -0
  264. package/src/utils/onboarding.ts +229 -0
  265. package/src/utils/prompts.ts +114 -0
  266. package/src/utils/run-commands.ts +112 -0
  267. package/src/utils/run-scripts.ts +142 -0
  268. package/src/utils/sanitize.ts +98 -0
  269. package/src/utils/secrets.ts +122 -0
  270. package/src/utils/shell-escape.ts +40 -0
  271. package/src/utils/utf8.ts +79 -0
  272. package/src/utils/workspace-state.ts +47 -0
  273. package/src/web/README.md +73 -0
  274. package/src/web/bun.lock +575 -0
  275. package/src/web/eslint.config.js +23 -0
  276. package/src/web/index.html +16 -0
  277. package/src/web/package.json +37 -0
  278. package/src/web/public/vite.svg +1 -0
  279. package/src/web/src/App.tsx +604 -0
  280. package/src/web/src/assets/react.svg +1 -0
  281. package/src/web/src/components/Terminal.tsx +207 -0
  282. package/src/web/src/hooks/useRelayConnection.ts +224 -0
  283. package/src/web/src/hooks/useTerminal.ts +699 -0
  284. package/src/web/src/index.css +55 -0
  285. package/src/web/src/lib/crypto/__tests__/web-terminal.test.ts +1158 -0
  286. package/src/web/src/lib/crypto/frames.ts +205 -0
  287. package/src/web/src/lib/crypto/handshake.ts +396 -0
  288. package/src/web/src/lib/crypto/identity.ts +128 -0
  289. package/src/web/src/lib/crypto/keyexchange.ts +246 -0
  290. package/src/web/src/lib/crypto/relay-signing.ts +53 -0
  291. package/src/web/src/lib/invite.ts +58 -0
  292. package/src/web/src/lib/storage/identity-store.ts +94 -0
  293. package/src/web/src/main.tsx +10 -0
  294. package/src/web/src/types/identity.ts +45 -0
  295. package/src/web/tsconfig.app.json +28 -0
  296. package/src/web/tsconfig.json +7 -0
  297. package/src/web/tsconfig.node.json +26 -0
  298. package/src/web/vite.config.ts +31 -0
  299. package/todo-security.md +92 -0
  300. package/tsconfig.json +23 -0
  301. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
  302. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
  303. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
  304. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
  305. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
  306. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
  307. package/worker/bun.lock +237 -0
  308. package/worker/package.json +22 -0
  309. package/worker/schema.sql +96 -0
  310. package/worker/src/handlers/auth.ts +451 -0
  311. package/worker/src/handlers/subdomains.ts +376 -0
  312. package/worker/src/handlers/user.ts +98 -0
  313. package/worker/src/index.ts +70 -0
  314. package/worker/src/middleware/auth.ts +152 -0
  315. package/worker/src/services/cloudflare.ts +609 -0
  316. package/worker/src/types.ts +96 -0
  317. package/worker/tsconfig.json +15 -0
  318. package/worker/wrangler.toml +26 -0
@@ -0,0 +1,1347 @@
1
+ # Gitspace Infrastructure Architecture
2
+
3
+ > **⚠️ FUTURE VISION DOCUMENT**
4
+ >
5
+ > This document describes a **planned future architecture** that is **not yet implemented**.
6
+ > The current implementation uses a simpler model with direct PTY sessions and WebSocket relay.
7
+ > See [RELAY.md](./RELAY.md) and [GETTING-STARTED.md](./GETTING-STARTED.md) for the current implementation.
8
+
9
+ ---
10
+
11
+ This document describes the infrastructure architecture for gitspace.sh - a platform for running development environments, CI/CD runners, and preview deployments using Firecracker microVMs.
12
+
13
+ ## Table of Contents
14
+
15
+ 1. [Vision](#vision)
16
+ 2. [Architecture Overview](#architecture-overview)
17
+ 3. [Orchestration Options](#orchestration-options)
18
+ 4. [Flintlock: MicroVM Management](#flintlock-microvm-management)
19
+ 5. [Nomad: Cluster Orchestration](#nomad-cluster-orchestration)
20
+ 6. [Firecracker: The MicroVM Runtime](#firecracker-the-microvm-runtime)
21
+ 7. [Image Build Pipeline](#image-build-pipeline)
22
+ 8. [Storage Model](#storage-model)
23
+ 9. [Networking Model](#networking-model)
24
+ 10. [Cloudflare Tunnels: Public Ingress](#cloudflare-tunnels-public-ingress)
25
+ 11. [GCP Integration](#gcp-integration)
26
+ 12. [Local Development: Mac Parity](#local-development-mac-parity)
27
+ 13. [Components to Build](#components-to-build)
28
+ 14. [Decision Matrix](#decision-matrix)
29
+
30
+ ---
31
+
32
+ ## Vision
33
+
34
+ Gitspaces are lightweight, isolated environments that can be used for:
35
+
36
+ | Use Case | Description | Lifecycle |
37
+ |----------|-------------|-----------|
38
+ | **Dev Environment** | Interactive terminal access to a workspace | Long-running, persistent storage |
39
+ | **CI Runner** | Execute tests/builds on push/PR | Ephemeral, dies after job |
40
+ | **Preview Environment** | Run app for PR review | Medium-lived, public URL |
41
+
42
+ **Key Goals:**
43
+ - Scale to zero when idle ($0 cost)
44
+ - Use spot/preemptible instances for 60-90% cost savings
45
+ - Same environment from local dev → CI → preview
46
+ - E2E encrypted terminal access via gitspace.sh relay
47
+ - Simple: just write scripts, no GitHub Actions YAML ceremony
48
+
49
+ ---
50
+
51
+ ## Architecture Overview
52
+
53
+ ```
54
+ ┌─────────────────────────────────────────────────────────────────────────────┐
55
+ │ USER LAYER │
56
+ │ │
57
+ │ spaces CLI ─────► Terminal access (E2E encrypted) │
58
+ │ Browser ─────────► Preview URLs (https://pr-123.preview.gitspace.sh) │
59
+ │ GitHub ──────────► Webhooks (push, PR events) │
60
+ └──────────────────────────────────────────────┬──────────────────────────────┘
61
+
62
+
63
+ ┌──────────────────────────────────────────────────────────────────────────────┐
64
+ │ CONTROL PLANE │
65
+ │ │
66
+ │ gitspace.sh relay │
67
+ │ ├── User authentication (API keys) │
68
+ │ ├── WebSocket relay (E2E encrypted terminal streams) │
69
+ │ ├── GitHub webhook handler │
70
+ │ ├── Job scheduler (submits to Nomad or custom) │
71
+ │ ├── Host provisioner (GCP API for scale up/down) │
72
+ │ └── State database (Postgres) │
73
+ │ │
74
+ │ Nomad Server (optional, for multi-host) │
75
+ │ ├── Job scheduling and bin packing │
76
+ │ ├── Health monitoring │
77
+ │ └── Cluster state │
78
+ └──────────────────────────────────────────────┬───────────────────────────────┘
79
+
80
+
81
+ ┌──────────────────────────────────────────────────────────────────────────────┐
82
+ │ DATA PLANE (Hosts) │
83
+ │ │
84
+ │ Each host runs: │
85
+ │ ├── Nomad Client (receives jobs, reports capacity) │
86
+ │ ├── Flintlock (manages Firecracker VMs via gRPC) │
87
+ │ ├── containerd (pulls OCI images) │
88
+ │ ├── cloudflared (tunnels for preview URLs) │
89
+ │ └── Firecracker VMs (the actual gitspaces) │
90
+ │ │
91
+ │ Hosts can be: │
92
+ │ ├── GCP Spot VMs (cheap, can be preempted) │
93
+ │ ├── GCP On-Demand VMs (reliable, more expensive) │
94
+ │ ├── Latitude.sh Bare Metal (dedicated, hourly billing) │
95
+ │ ├── Your own computer (self-hosted) │
96
+ │ └── Mac with Lima (local development) │
97
+ └──────────────────────────────────────────────────────────────────────────────┘
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Orchestration Options
103
+
104
+ We have three main options for orchestrating gitspaces across hosts:
105
+
106
+ ### Option A: Custom Orchestration (DIY)
107
+
108
+ Build our own scheduler in the relay.
109
+
110
+ ```
111
+ ┌─────────────────────────────────────────────────────────────────────────────┐
112
+ │ gitspace.sh relay │
113
+ │ │
114
+ │ ┌────────────────────────────────────────────────────────────────────────┐ │
115
+ │ │ Custom Scheduler │ │
116
+ │ │ │ │
117
+ │ │ - Host registry (which hosts are available) │ │
118
+ │ │ - Capacity tracking (CPU/mem per host) │ │
119
+ │ │ - Simple scheduling (round-robin with capacity check) │ │
120
+ │ │ - Job queue (in-memory or Redis) │ │
121
+ │ └────────────────────────────────────────────────────────────────────────┘ │
122
+ │ │
123
+ │ Talks directly to gitspace-daemon on each host via WebSocket/gRPC │
124
+ └─────────────────────────────────────────────────────────────────────────────┘
125
+
126
+
127
+ ┌─────────────────────────────────────────────────────────────────────────────┐
128
+ │ gitspace-daemon (runs on each host) │
129
+ │ │
130
+ │ - Manages Firecracker VMs directly (or via Flintlock) │
131
+ │ - Reports capacity to relay │
132
+ │ - Handles volume management │
133
+ │ - Manages Cloudflare tunnels │
134
+ └─────────────────────────────────────────────────────────────────────────────┘
135
+ ```
136
+
137
+ **Pros:**
138
+ - Full control
139
+ - No external dependencies
140
+ - Simpler for single-host
141
+ - Fun to build
142
+
143
+ **Cons:**
144
+ - Must build scheduling, health checks, failover
145
+ - More code to maintain
146
+ - Harder to get right at scale
147
+
148
+ **Best for:** Starting out, single host, learning
149
+
150
+ ---
151
+
152
+ ### Option B: Flintlock + Nomad
153
+
154
+ Use Flintlock for VM lifecycle, Nomad for scheduling.
155
+
156
+ ```
157
+ ┌─────────────────────────────────────────────────────────────────────────────┐
158
+ │ gitspace.sh relay │
159
+ │ │
160
+ │ - Submits jobs to Nomad │
161
+ │ - Manages GCP hosts (scale up/down) │
162
+ │ - WebSocket relay for terminal access │
163
+ └──────────────────────────────────────────────┬──────────────────────────────┘
164
+
165
+
166
+ ┌─────────────────────────────────────────────────────────────────────────────┐
167
+ │ Nomad Cluster │
168
+ │ │
169
+ │ Server: Scheduling, state, health monitoring │
170
+ │ Client: Runs on each host, executes jobs │
171
+ └──────────────────────────────────────────────┬──────────────────────────────┘
172
+
173
+
174
+ ┌─────────────────────────────────────────────────────────────────────────────┐
175
+ │ Each Host │
176
+ │ │
177
+ │ Nomad Client ──► Flintlock ──► Firecracker VMs │
178
+ │ │ │
179
+ │ ▼ │
180
+ │ containerd (OCI images) │
181
+ └─────────────────────────────────────────────────────────────────────────────┘
182
+ ```
183
+
184
+ **Pros:**
185
+ - Battle-tested scheduling (Nomad)
186
+ - Clean VM lifecycle management (Flintlock)
187
+ - OCI images for everything (containerd)
188
+ - Health checks, restarts, bin packing built-in
189
+
190
+ **Cons:**
191
+ - More moving parts
192
+ - Flintlock is community-maintained (was Weaveworks)
193
+ - Learning curve for Nomad
194
+
195
+ **Best for:** Multi-host production, scaling
196
+
197
+ ---
198
+
199
+ ### Option C: Nomad Only (with raw_exec or custom driver)
200
+
201
+ Use Nomad for scheduling, manage Firecracker directly.
202
+
203
+ ```
204
+ ┌─────────────────────────────────────────────────────────────────────────────┐
205
+ │ Nomad Cluster │
206
+ │ │
207
+ │ Jobs use raw_exec or firecracker-task-driver to run VMs │
208
+ └──────────────────────────────────────────────┬──────────────────────────────┘
209
+
210
+
211
+ ┌─────────────────────────────────────────────────────────────────────────────┐
212
+ │ Each Host │
213
+ │ │
214
+ │ Nomad Client ──► raw_exec ──► firecracker binary │
215
+ │ │ │
216
+ │ ▼ │
217
+ │ Firecracker VMs │
218
+ └─────────────────────────────────────────────────────────────────────────────┘
219
+ ```
220
+
221
+ **Pros:**
222
+ - Nomad handles scheduling
223
+ - Fewer components than Flintlock
224
+ - Community firecracker-task-driver available
225
+
226
+ **Cons:**
227
+ - Must manage images ourselves (no containerd integration)
228
+ - firecracker-task-driver is also community-maintained
229
+ - More manual VM configuration
230
+
231
+ **Best for:** Middle ground, if Flintlock feels too heavy
232
+
233
+ ---
234
+
235
+ ## Flintlock: MicroVM Management
236
+
237
+ [Flintlock](https://github.com/liquidmetal-dev/flintlock) is a gRPC service for managing Firecracker/Cloud Hypervisor VMs on a single host.
238
+
239
+ ### What Flintlock Does
240
+
241
+ | Capability | Description |
242
+ |------------|-------------|
243
+ | **VM Lifecycle** | Create, start, stop, delete microVMs |
244
+ | **OCI Images** | Pull kernel and rootfs from container registries |
245
+ | **containerd Integration** | Uses containerd for image management and snapshots |
246
+ | **Networking** | Configures TAP devices, CNI plugins |
247
+ | **Metadata** | Injects cloud-init/ignition for VM configuration |
248
+ | **gRPC API** | Clean, well-defined API for all operations |
249
+
250
+ ### What Flintlock Does NOT Do
251
+
252
+ | Capability | Needs |
253
+ |------------|-------|
254
+ | Multi-host scheduling | Nomad or custom |
255
+ | Scale up/down hosts | Custom + cloud API |
256
+ | Cross-host networking | CNI/Tailscale/Cloudflare |
257
+ | Persistent volumes across hosts | Custom |
258
+
259
+ ### Flintlock Architecture
260
+
261
+ ```
262
+ ┌─────────────────────────────────────────────────────────────────────────────┐
263
+ │ HOST │
264
+ │ │
265
+ │ ┌─────────────────────────────────────────────────────────────────────────┐│
266
+ │ │ flintlockd ││
267
+ │ │ ││
268
+ │ │ gRPC API (:9090) ││
269
+ │ │ ├── CreateMicroVM(spec) ││
270
+ │ │ ├── DeleteMicroVM(id) ││
271
+ │ │ ├── GetMicroVM(id) ││
272
+ │ │ └── ListMicroVMs() ││
273
+ │ │ ││
274
+ │ │ Uses containerd for: ││
275
+ │ │ ├── Pulling OCI images (kernel, rootfs) ││
276
+ │ │ └── Managing devicemapper snapshots ││
277
+ │ └─────────────────────────────────────────────────────────────────────────┘│
278
+ │ │ │
279
+ │ ┌───────────────────┼───────────────────┐ │
280
+ │ ▼ ▼ ▼ │
281
+ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
282
+ │ │ Firecracker │ │ Firecracker │ │ Firecracker │ │
283
+ │ │ MicroVM │ │ MicroVM │ │ MicroVM │ │
284
+ │ └─────────────┘ └─────────────┘ └─────────────┘ │
285
+ │ │
286
+ └──────────────────────────────────────────────────────────────────────────────┘
287
+ ```
288
+
289
+ ### Flintlock MicroVM Spec
290
+
291
+ ```json
292
+ {
293
+ "id": "gitspace-abc123",
294
+ "namespace": "gitspaces",
295
+ "labels": {
296
+ "user_id": "user-xyz",
297
+ "workspace": "my-project"
298
+ },
299
+ "vcpu": 2,
300
+ "memory_in_mb": 2048,
301
+ "kernel": {
302
+ "image": "ghcr.io/gitspace/kernel:5.10",
303
+ "filename": "vmlinux"
304
+ },
305
+ "root_volume": {
306
+ "id": "root",
307
+ "is_read_only": false,
308
+ "source": {
309
+ "container_source": "ghcr.io/gitspace/ubuntu:22.04"
310
+ }
311
+ },
312
+ "additional_volumes": [
313
+ {
314
+ "id": "workspace",
315
+ "is_read_only": false,
316
+ "mount_point": "/workspace",
317
+ "source": {
318
+ "host_path": "/var/lib/gitspace/volumes/user-xyz/my-project"
319
+ }
320
+ }
321
+ ],
322
+ "interfaces": [
323
+ {
324
+ "device_id": "eth0",
325
+ "type": "TAP"
326
+ }
327
+ ],
328
+ "metadata": {
329
+ "user-data": "<base64-encoded-cloud-init>"
330
+ }
331
+ }
332
+ ```
333
+
334
+ ### Flintlock Configuration
335
+
336
+ ```yaml
337
+ # /etc/flintlock/config.yaml
338
+
339
+ containerd-socket: /run/containerd/containerd.sock
340
+ grpc-endpoint: 0.0.0.0:9090
341
+ state-dir: /var/lib/flintlock/vm
342
+ default-vmm: firecracker
343
+ firecracker-bin: /usr/local/bin/firecracker
344
+ bridge-name: flbr0
345
+ ```
346
+
347
+ ---
348
+
349
+ ## Nomad: Cluster Orchestration
350
+
351
+ [Nomad](https://www.nomadproject.io/) is HashiCorp's workload orchestrator. Simpler than Kubernetes, supports VMs and other non-container workloads.
352
+
353
+ ### Nomad Architecture
354
+
355
+ ```
356
+ ┌─────────────────────────────────────────────────────────────────────────────┐
357
+ │ Nomad Server Cluster │
358
+ │ │
359
+ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
360
+ │ │ Nomad Server │ │ Nomad Server │ │ Nomad Server │ │
361
+ │ │ (Leader) │◄─┤ (Follower) │◄─┤ (Follower) │ │
362
+ │ └───────────────┘ └───────────────┘ └───────────────┘ │
363
+ │ │
364
+ │ Responsibilities: │
365
+ │ ├── Leader election (Raft consensus) │
366
+ │ ├── Job scheduling and placement │
367
+ │ ├── Cluster state │
368
+ │ └── Health monitoring │
369
+ └──────────────────────────────────────────────┬──────────────────────────────┘
370
+
371
+ ┌────────────────────────────────┼────────────────────────────────┐
372
+ │ │ │
373
+ ▼ ▼ ▼
374
+ ┌───────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐
375
+ │ Nomad Client │ │ Nomad Client │ │ Nomad Client │
376
+ │ (Host 1) │ │ (Host 2) │ │ (Host 3) │
377
+ │ │ │ │ │ │
378
+ │ - Receives jobs │ │ - Receives jobs │ │ - Receives jobs │
379
+ │ - Reports capacity │ │ - Reports capacity │ │ - Reports capacity │
380
+ │ - Runs task drivers │ │ - Runs task drivers │ │ - Runs task drivers │
381
+ └───────────────────────┘ └───────────────────────┘ └───────────────────────┘
382
+ ```
383
+
384
+ ### Nomad Server Configuration
385
+
386
+ ```hcl
387
+ # /etc/nomad.d/server.hcl
388
+
389
+ datacenter = "gcp-us-central1"
390
+ data_dir = "/opt/nomad/data"
391
+
392
+ server {
393
+ enabled = true
394
+ bootstrap_expect = 3
395
+ encrypt = "GOSSIP_ENCRYPTION_KEY"
396
+ }
397
+
398
+ addresses {
399
+ http = "0.0.0.0"
400
+ rpc = "0.0.0.0"
401
+ serf = "0.0.0.0"
402
+ }
403
+
404
+ acl {
405
+ enabled = true
406
+ }
407
+ ```
408
+
409
+ ### Nomad Client Configuration
410
+
411
+ ```hcl
412
+ # /etc/nomad.d/client.hcl
413
+
414
+ datacenter = "gcp-us-central1"
415
+ data_dir = "/opt/nomad/data"
416
+
417
+ client {
418
+ enabled = true
419
+
420
+ meta {
421
+ "zone" = "us-central1-a"
422
+ "spot" = "true"
423
+ "has_flintlock" = "true"
424
+ }
425
+
426
+ host_volume "gitspace-volumes" {
427
+ path = "/var/lib/gitspace/volumes"
428
+ read_only = false
429
+ }
430
+ }
431
+
432
+ servers = ["nomad-server.internal:4647"]
433
+
434
+ plugin "raw_exec" {
435
+ config {
436
+ enabled = true
437
+ }
438
+ }
439
+ ```
440
+
441
+ ### Nomad Job Types
442
+
443
+ **Service Jobs** (long-running):
444
+ ```hcl
445
+ job "gitspace-dev" {
446
+ type = "service"
447
+ # ... runs until stopped
448
+ }
449
+ ```
450
+
451
+ **Batch Jobs** (run to completion):
452
+ ```hcl
453
+ job "gitspace-ci" {
454
+ type = "batch"
455
+ # ... runs until exit, with timeout
456
+ }
457
+ ```
458
+
459
+ **Parameterized Jobs** (templates):
460
+ ```hcl
461
+ job "gitspace" {
462
+ type = "service"
463
+
464
+ parameterized {
465
+ meta_required = ["user_id", "workspace_id"]
466
+ }
467
+
468
+ # Each dispatch creates a child job
469
+ }
470
+ ```
471
+
472
+ ### Example: Gitspace Service Job
473
+
474
+ ```hcl
475
+ job "gitspace" {
476
+ type = "service"
477
+ datacenters = ["gcp-us-central1"]
478
+
479
+ parameterized {
480
+ meta_required = ["user_id", "workspace_id", "workspace_name"]
481
+ meta_optional = ["cpu", "memory", "zone"]
482
+ }
483
+
484
+ group "microvm" {
485
+ count = 1
486
+
487
+ constraint {
488
+ attribute = "${meta.has_flintlock}"
489
+ value = "true"
490
+ }
491
+
492
+ volume "workspace" {
493
+ type = "host"
494
+ source = "gitspace-volumes"
495
+ }
496
+
497
+ task "vm" {
498
+ driver = "raw_exec"
499
+
500
+ config {
501
+ command = "/usr/local/bin/flintlock-ctl"
502
+ args = ["microvm", "create", "--json-spec", "${NOMAD_TASK_DIR}/spec.json"]
503
+ }
504
+
505
+ template {
506
+ destination = "local/spec.json"
507
+ data = <<EOF
508
+ {
509
+ "id": "{{ env "NOMAD_META_workspace_id" }}",
510
+ "namespace": "gitspaces",
511
+ "vcpu": {{ env "NOMAD_META_cpu" | default "2" }},
512
+ "memory_in_mb": {{ env "NOMAD_META_memory" | default "2048" }},
513
+ "kernel": {
514
+ "image": "ghcr.io/gitspace/kernel:5.10"
515
+ },
516
+ "root_volume": {
517
+ "source": {
518
+ "container_source": "ghcr.io/gitspace/ubuntu:22.04"
519
+ }
520
+ }
521
+ }
522
+ EOF
523
+ }
524
+
525
+ resources {
526
+ cpu = 2000
527
+ memory = 2048
528
+ }
529
+ }
530
+ }
531
+ }
532
+ ```
533
+
534
+ ### Example: CI Batch Job
535
+
536
+ ```hcl
537
+ job "ci" {
538
+ type = "batch"
539
+ datacenters = ["gcp-us-central1"]
540
+
541
+ parameterized {
542
+ meta_required = ["repo", "commit_sha", "job_id"]
543
+ }
544
+
545
+ group "runner" {
546
+ task "run" {
547
+ driver = "raw_exec"
548
+
549
+ config {
550
+ command = "/usr/local/bin/gitspace-ci-runner"
551
+ args = [
552
+ "--job-id", "${NOMAD_META_job_id}",
553
+ "--repo", "${NOMAD_META_repo}",
554
+ "--commit", "${NOMAD_META_commit_sha}",
555
+ ]
556
+ }
557
+
558
+ resources {
559
+ cpu = 4000
560
+ memory = 8192
561
+ }
562
+ }
563
+ }
564
+ }
565
+ ```
566
+
567
+ ---
568
+
569
+ ## Firecracker: The MicroVM Runtime
570
+
571
+ [Firecracker](https://firecracker-microvm.github.io/) is a lightweight VMM (Virtual Machine Monitor) designed for serverless and container workloads.
572
+
573
+ ### Key Characteristics
574
+
575
+ | Property | Value |
576
+ |----------|-------|
577
+ | **Boot time** | <125ms |
578
+ | **Memory overhead** | <5 MiB per VM |
579
+ | **Creation rate** | Up to 150 VMs/second/host |
580
+ | **Isolation** | Hardware virtualization (KVM) |
581
+ | **Supported arch** | x86_64, aarch64 |
582
+
583
+ ### Firecracker Configuration
584
+
585
+ ```json
586
+ {
587
+ "boot-source": {
588
+ "kernel_image_path": "/var/lib/firecracker/vmlinux",
589
+ "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
590
+ },
591
+ "drives": [
592
+ {
593
+ "drive_id": "rootfs",
594
+ "path_on_host": "/var/lib/firecracker/rootfs.ext4",
595
+ "is_root_device": true,
596
+ "is_read_only": false
597
+ },
598
+ {
599
+ "drive_id": "workspace",
600
+ "path_on_host": "/var/lib/gitspace/volumes/user-xyz/workspace.ext4",
601
+ "is_root_device": false,
602
+ "is_read_only": false
603
+ }
604
+ ],
605
+ "machine-config": {
606
+ "vcpu_count": 2,
607
+ "mem_size_mib": 2048
608
+ },
609
+ "network-interfaces": [
610
+ {
611
+ "iface_id": "eth0",
612
+ "guest_mac": "AA:FC:00:00:00:01",
613
+ "host_dev_name": "tap0"
614
+ }
615
+ ]
616
+ }
617
+ ```
618
+
619
+ ### Requirements
620
+
621
+ - Linux host with KVM enabled
622
+ - `/dev/kvm` accessible
623
+ - For nested virtualization (VMs inside VMs):
624
+ - GCP: `--enable-nested-virtualization` on n2 instances
625
+ - Mac: macOS 15+ with M2/M3 chip, Lima with `nestedVirtualization: true`
626
+
627
+ ---
628
+
629
+ ## Image Build Pipeline
630
+
631
+ We own the image pipeline to ensure parity across all environments.
632
+
633
+ ### Pipeline Overview
634
+
635
+ ```
636
+ ┌─────────────────────────────────────────────────────────────────────────────┐
637
+ │ IMAGE BUILD PIPELINE │
638
+ │ │
639
+ │ Dockerfile │
640
+ │ │ │
641
+ │ ▼ │
642
+ │ BuildKit (multi-arch: amd64, arm64) │
643
+ │ │ │
644
+ │ ▼ │
645
+ │ OCI Image │
646
+ │ │ │
647
+ │ ├──────────────────────────────────────┐ │
648
+ │ ▼ ▼ │
649
+ │ Push to Registry Convert to rootfs │
650
+ │ (ghcr.io/gitspace/...) (for direct Firecracker use) │
651
+ │ │ │ │
652
+ │ ▼ ▼ │
653
+ │ Flintlock pulls via containerd Manual FC config │
654
+ │ │
655
+ └─────────────────────────────────────────────────────────────────────────────┘
656
+ ```
657
+
658
+ ### Base Image Dockerfile
659
+
660
+ ```dockerfile
661
+ # images/ubuntu-base/Dockerfile
662
+
663
+ FROM ubuntu:22.04
664
+
665
+ # System packages
666
+ RUN apt-get update && apt-get install -y \
667
+ curl \
668
+ git \
669
+ build-essential \
670
+ sudo \
671
+ openssh-server \
672
+ && rm -rf /var/lib/apt/lists/*
673
+
674
+ # Create gitspace user
675
+ RUN useradd -m -s /bin/bash gitspace && \
676
+ echo "gitspace ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
677
+
678
+ # Install common dev tools
679
+ RUN curl -fsSL https://bun.sh/install | bash
680
+ RUN curl -fsSL https://get.docker.com | bash
681
+
682
+ # tmux-lite-server for terminal access
683
+ COPY --from=gitspace/tmux-lite:latest /usr/local/bin/tmux-lite-server /usr/local/bin/
684
+
685
+ # Cloud-init for configuration
686
+ RUN apt-get update && apt-get install -y cloud-init
687
+
688
+ # Startup script
689
+ COPY startup.sh /usr/local/bin/
690
+ RUN chmod +x /usr/local/bin/startup.sh
691
+
692
+ CMD ["/usr/local/bin/startup.sh"]
693
+ ```
694
+
695
+ ### Build Script
696
+
697
+ ```bash
698
+ #!/bin/bash
699
+ # scripts/build-images.sh
700
+
701
+ set -e
702
+
703
+ REGISTRY="ghcr.io/gitspace"
704
+ VERSION="${1:-latest}"
705
+
706
+ # Build multi-arch
707
+ docker buildx build \
708
+ --platform linux/amd64,linux/arm64 \
709
+ --push \
710
+ -t "${REGISTRY}/ubuntu:${VERSION}" \
711
+ -f images/ubuntu-base/Dockerfile \
712
+ images/ubuntu-base/
713
+
714
+ # Build kernel image
715
+ docker buildx build \
716
+ --platform linux/amd64,linux/arm64 \
717
+ --push \
718
+ -t "${REGISTRY}/kernel:5.10" \
719
+ -f images/kernel/Dockerfile \
720
+ images/kernel/
721
+
722
+ echo "Images pushed to ${REGISTRY}"
723
+ ```
724
+
725
+ ### Converting OCI to rootfs (for direct Firecracker use)
726
+
727
+ ```bash
728
+ #!/bin/bash
729
+ # scripts/oci-to-rootfs.sh
730
+
731
+ IMAGE="ghcr.io/gitspace/ubuntu:latest"
732
+ OUTPUT="rootfs.ext4"
733
+ SIZE_MB=4096
734
+
735
+ # Pull and extract
736
+ skopeo copy "docker://${IMAGE}" "oci:image:latest"
737
+ umoci unpack --image image:latest bundle
738
+
739
+ # Create ext4 filesystem
740
+ dd if=/dev/zero of="${OUTPUT}" bs=1M count="${SIZE_MB}"
741
+ mkfs.ext4 "${OUTPUT}"
742
+
743
+ # Mount and copy
744
+ mkdir -p /tmp/rootfs
745
+ mount -o loop "${OUTPUT}" /tmp/rootfs
746
+ cp -a bundle/rootfs/* /tmp/rootfs/
747
+ umount /tmp/rootfs
748
+
749
+ echo "Created ${OUTPUT}"
750
+ ```
751
+
752
+ ---
753
+
754
+ ## Storage Model
755
+
756
+ ### Overview
757
+
758
+ ```
759
+ ┌─────────────────────────────────────────────────────────────────────────────┐
760
+ │ STORAGE MODEL │
761
+ │ │
762
+ │ Root Volume (ephemeral) │
763
+ │ ├── OCI image pulled by containerd │
764
+ │ ├── Read-write, but reset on VM restart │
765
+ │ └── Contains OS, tools, runtime │
766
+ │ │
767
+ │ Workspace Volume (persistent) │
768
+ │ ├── Sparse ext4 file on host │
769
+ │ ├── Attached as /dev/vdb, mounted at /workspace │
770
+ │ ├── Survives VM restarts │
771
+ │ └── User's code, data, config │
772
+ │ │
773
+ └─────────────────────────────────────────────────────────────────────────────┘
774
+ ```
775
+
776
+ ### Host Storage Layout
777
+
778
+ ```
779
+ /var/lib/gitspace/
780
+ ├── images/ # Cached rootfs images
781
+ │ ├── ubuntu-22.04.ext4
782
+ │ └── node-20.ext4
783
+
784
+ ├── kernel/ # Kernel images
785
+ │ └── vmlinux-5.10
786
+
787
+ └── volumes/ # Persistent workspace volumes (btrfs)
788
+ ├── user-abc/
789
+ │ ├── workspace-1.ext4 # Sparse file, grows on demand
790
+ │ └── workspace-2.ext4
791
+ └── user-def/
792
+ └── workspace-1.ext4
793
+ ```
794
+
795
+ ### Btrfs for Volume Management
796
+
797
+ ```bash
798
+ # Initial setup
799
+ mkfs.btrfs /dev/sdb
800
+ mount /dev/sdb /var/lib/gitspace/volumes
801
+
802
+ # Create workspace with quota
803
+ btrfs subvolume create /var/lib/gitspace/volumes/@user-abc-ws-1
804
+ btrfs qgroup limit 20G /var/lib/gitspace/volumes/@user-abc-ws-1
805
+
806
+ # Create sparse ext4 file (100GB logical, ~30MB actual)
807
+ truncate -s 100G /var/lib/gitspace/volumes/@user-abc-ws-1/workspace.ext4
808
+ mkfs.ext4 -F workspace.ext4
809
+
810
+ # Snapshot for backup
811
+ btrfs subvolume snapshot \
812
+ /var/lib/gitspace/volumes/@user-abc-ws-1 \
813
+ /var/lib/gitspace/snapshots/@user-abc-ws-1-$(date +%Y%m%d)
814
+ ```
815
+
816
+ ### Storage Tiers
817
+
818
+ | Tier | Quota per Workspace | Max Workspaces |
819
+ |------|---------------------|----------------|
820
+ | Free | 5 GB | 2 |
821
+ | Pro | 20 GB | 10 |
822
+ | Team | 50 GB | 50 |
823
+
824
+ ### GCP Persistent Disk Setup
825
+
826
+ ```bash
827
+ # Create persistent disk
828
+ gcloud compute disks create gitspace-volumes \
829
+ --size=1TB \
830
+ --type=pd-ssd \
831
+ --zone=us-central1-a
832
+
833
+ # Attach to VM
834
+ gcloud compute instances attach-disk gitspace-host \
835
+ --disk=gitspace-volumes \
836
+ --zone=us-central1-a
837
+
838
+ # Inside VM: format as btrfs
839
+ mkfs.btrfs /dev/sdb
840
+ mount /dev/sdb /var/lib/gitspace/volumes
841
+ ```
842
+
843
+ ---
844
+
845
+ ## Networking Model
846
+
847
+ ### Overview
848
+
849
+ ```
850
+ ┌─────────────────────────────────────────────────────────────────────────────┐
851
+ │ NETWORKING MODEL │
852
+ │ │
853
+ │ Terminal Access: │
854
+ │ └── gitspace.sh relay (WebSocket, E2E encrypted) │
855
+ │ └── User ◄──wss──► Relay ◄──wss──► tmux-lite-server in VM │
856
+ │ │
857
+ │ Preview URLs (Public HTTP): │
858
+ │ └── Cloudflare Tunnel (outbound only) │
859
+ │ └── User ◄──https──► Cloudflare ◄──tunnel──► App in VM │
860
+ │ │
861
+ │ VM Internet Access (Outbound): │
862
+ │ └── NAT via host (iptables masquerade) │
863
+ │ └── VM ──► TAP ──► Bridge ──► Host ──► Internet │
864
+ │ │
865
+ │ VM-to-VM (Same Host): │
866
+ │ └── Bridge network │
867
+ │ └── VM1 ◄──► Bridge ◄──► VM2 │
868
+ │ │
869
+ │ VM-to-VM (Cross Host): │
870
+ │ └── Not needed! Use public preview URLs │
871
+ │ └── Frontend calls https://api-pr-123.preview.gitspace.sh │
872
+ │ │
873
+ └─────────────────────────────────────────────────────────────────────────────┘
874
+ ```
875
+
876
+ ### TAP/Bridge Setup (per host)
877
+
878
+ ```bash
879
+ # Create bridge
880
+ ip link add name flbr0 type bridge
881
+ ip addr add 10.100.0.1/24 dev flbr0
882
+ ip link set flbr0 up
883
+
884
+ # Enable IP forwarding
885
+ echo 1 > /proc/sys/net/ipv4/ip_forward
886
+
887
+ # NAT for outbound traffic
888
+ iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
889
+ iptables -A FORWARD -i flbr0 -o eth0 -j ACCEPT
890
+ iptables -A FORWARD -i eth0 -o flbr0 -m state --state RELATED,ESTABLISHED -j ACCEPT
891
+ ```
892
+
893
+ ### Per-VM TAP Device
894
+
895
+ ```bash
896
+ # Created by Flintlock automatically
897
+ ip tuntap add dev tap0 mode tap
898
+ ip link set tap0 master flbr0
899
+ ip link set tap0 up
900
+
901
+ # VM gets IP via DHCP or static config
902
+ # Inside VM: eth0 = 10.100.0.2/24, gateway = 10.100.0.1
903
+ ```
904
+
905
+ ---
906
+
907
+ ## Cloudflare Tunnels: Public Ingress
908
+
909
+ ### How It Works
910
+
911
+ ```
912
+ ┌─────────────────────────────────────────────────────────────────────────────┐
913
+ │ CLOUDFLARE TUNNEL │
914
+ │ │
915
+ │ User Browser │
916
+ │ │ │
917
+ │ │ https://pr-123.preview.gitspace.sh │
918
+ │ ▼ │
919
+ │ ┌─────────────────────────────────────────────────────────────────────┐ │
920
+ │ │ Cloudflare Edge │ │
921
+ │ │ - SSL termination │ │
922
+ │ │ - DDoS protection │ │
923
+ │ │ - WAF │ │
924
+ │ │ - Routes by hostname │ │
925
+ │ └──────────────────────────────┬──────────────────────────────────────┘ │
926
+ │ │ │
927
+ │ │ Tunnel (outbound from origin) │
928
+ │ ▼ │
929
+ │ ┌─────────────────────────────────────────────────────────────────────┐ │
930
+ │ │ Host (gitspace-host-xyz) │ │
931
+ │ │ │ │
932
+ │ │ cloudflared ◄─────────────────────────────────────────────────────┤ │
933
+ │ │ │ │ │
934
+ │ │ │ localhost:8080 │ │
935
+ │ │ ▼ │ │
936
+ │ │ Firecracker VM (app running on :3000, mapped to host :8080) │ │
937
+ │ └─────────────────────────────────────────────────────────────────────┘ │
938
+ │ │
939
+ └─────────────────────────────────────────────────────────────────────────────┘
940
+ ```
941
+
942
+ ### Tunnel Configuration
943
+
944
+ ```yaml
945
+ # /etc/cloudflared/config.yml
946
+
947
+ tunnel: gitspace-previews
948
+ credentials-file: /etc/cloudflared/creds.json
949
+
950
+ ingress:
951
+ # Wildcard for preview URLs
952
+ - hostname: "*.preview.gitspace.sh"
953
+ service: http://localhost:8080
954
+
955
+ # Catch-all
956
+ - service: http_status:404
957
+ ```
958
+
959
+ ### Per-Gitspace Tunnel Management
960
+
961
+ ```typescript
962
+ // In gitspace-daemon or relay
963
+
964
+ async function createPreviewTunnel(gitspaceId: string, port: number) {
965
+ const hostname = `${gitspaceId}.preview.gitspace.sh`;
966
+
967
+ // Create DNS record pointing to tunnel
968
+ await cloudflare.dns.create({
969
+ zone: 'gitspace.sh',
970
+ type: 'CNAME',
971
+ name: hostname,
972
+ content: 'tunnel-id.cfargotunnel.com',
973
+ proxied: true,
974
+ });
975
+
976
+ // Update tunnel ingress
977
+ await updateTunnelConfig(hostname, `http://localhost:${port}`);
978
+
979
+ return `https://${hostname}`;
980
+ }
981
+ ```
982
+
983
+ ### Pricing
984
+
985
+ | Tier | Tunnels | Cost |
986
+ |------|---------|------|
987
+ | Free | 50 | $0 |
988
+ | Pro | Unlimited | $20/mo |
989
+
990
+ ---
991
+
992
+ ## GCP Integration
993
+
994
+ ### Spot VMs for Cost Savings
995
+
996
+ | VM Type | On-Demand | Spot | Savings |
997
+ |---------|-----------|------|---------|
998
+ | n2-standard-2 | ~$50/mo | ~$15/mo | 70% |
999
+ | n2-standard-4 | ~$100/mo | ~$30/mo | 70% |
1000
+ | n2-standard-8 | ~$200/mo | ~$60/mo | 70% |
1001
+
1002
+ ### Provisioning a Host
1003
+
1004
+ ```typescript
1005
+ async function provisionHost(zone: string, spot: boolean) {
1006
+ const hostId = crypto.randomUUID();
1007
+
1008
+ await gcp.instances.insert({
1009
+ project: PROJECT_ID,
1010
+ zone,
1011
+ requestBody: {
1012
+ name: `gitspace-host-${hostId}`,
1013
+ machineType: `zones/${zone}/machineTypes/n2-standard-8`,
1014
+
1015
+ scheduling: {
1016
+ provisioningModel: spot ? 'SPOT' : 'STANDARD',
1017
+ instanceTerminationAction: 'DELETE',
1018
+ onHostMaintenance: 'TERMINATE',
1019
+ },
1020
+
1021
+ disks: [
1022
+ {
1023
+ boot: true,
1024
+ autoDelete: true,
1025
+ initializeParams: {
1026
+ sourceImage: `projects/${PROJECT_ID}/global/images/family/gitspace-host`,
1027
+ diskSizeGb: 100,
1028
+ },
1029
+ },
1030
+ {
1031
+ // Attached SSD for volumes
1032
+ autoDelete: false,
1033
+ initializeParams: {
1034
+ diskType: `zones/${zone}/diskTypes/pd-ssd`,
1035
+ diskSizeGb: 500,
1036
+ },
1037
+ },
1038
+ ],
1039
+
1040
+ networkInterfaces: [{
1041
+ network: 'global/networks/gitspace-vpc',
1042
+ accessConfigs: [{ type: 'ONE_TO_ONE_NAT' }],
1043
+ }],
1044
+
1045
+ metadata: {
1046
+ items: [
1047
+ { key: 'host-id', value: hostId },
1048
+ { key: 'startup-script-url', value: 'gs://gitspace-scripts/startup.sh' },
1049
+ ],
1050
+ },
1051
+
1052
+ tags: { items: ['gitspace-host', 'nomad-client'] },
1053
+
1054
+ serviceAccounts: [{
1055
+ email: `gitspace-host@${PROJECT_ID}.iam.gserviceaccount.com`,
1056
+ scopes: ['https://www.googleapis.com/auth/cloud-platform'],
1057
+ }],
1058
+ },
1059
+ });
1060
+
1061
+ return hostId;
1062
+ }
1063
+ ```
1064
+
1065
+ ### Auto-Scaling Logic
1066
+
1067
+ ```typescript
1068
+ async function checkAndScale() {
1069
+ const capacity = await getClusterCapacity();
1070
+ const pending = await getPendingAllocations();
1071
+
1072
+ const utilization = capacity.cpu.used / capacity.cpu.total;
1073
+
1074
+ // Scale up: pending jobs or high utilization
1075
+ if (pending.length > 0 || utilization > 0.8) {
1076
+ if (capacity.nodes < MAX_HOSTS) {
1077
+ await provisionHost(pickZone(), /* spot */ true);
1078
+ }
1079
+ }
1080
+
1081
+ // Scale down: low utilization
1082
+ if (utilization < 0.2 && capacity.nodes > MIN_HOSTS) {
1083
+ const idleHost = await findIdleHost();
1084
+ if (idleHost && await hasBeenIdleFor(idleHost, 10 * 60 * 1000)) {
1085
+ await terminateHost(idleHost);
1086
+ }
1087
+ }
1088
+ }
1089
+ ```
1090
+
1091
+ ### Handling Spot Preemption
1092
+
1093
+ ```bash
1094
+ #!/bin/bash
1095
+ # /usr/local/bin/preemption-handler.sh (runs on each host)
1096
+
1097
+ while true; do
1098
+ PREEMPTED=$(curl -s -H "Metadata-Flavor: Google" \
1099
+ http://metadata.google.internal/computeMetadata/v1/instance/preempted)
1100
+
1101
+ if [ "$PREEMPTED" = "TRUE" ]; then
1102
+ echo "Preemption notice, draining..."
1103
+
1104
+ # Drain Nomad node
1105
+ nomad node drain -self -enable -deadline 25s -force
1106
+
1107
+ # Stop Flintlock VMs gracefully
1108
+ flintlock-ctl microvm list | xargs -I {} flintlock-ctl microvm delete {}
1109
+
1110
+ exit 0
1111
+ fi
1112
+
1113
+ sleep 5
1114
+ done
1115
+ ```
1116
+
1117
+ ---
1118
+
1119
+ ## Local Development: Mac Parity
1120
+
1121
+ ### Requirements
1122
+
1123
+ - macOS 15 (Sequoia) or later
1124
+ - Apple Silicon M2 or M3 (with hardware nested virtualization)
1125
+ - Lima v1.0+
1126
+
1127
+ ### Architecture
1128
+
1129
+ ```
1130
+ ┌─────────────────────────────────────────────────────────────────────────────┐
1131
+ │ macOS Host │
1132
+ │ │
1133
+ │ ┌─────────────────────────────────────────────────────────────────────┐ │
1134
+ │ │ Lima VM (ARM64 Linux) │ │
1135
+ │ │ nestedVirtualization: true │ │
1136
+ │ │ │ │
1137
+ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │
1138
+ │ │ │ KVM │ │ │
1139
+ │ │ └──────────────────────────┬──────────────────────────────────┘ │ │
1140
+ │ │ │ │ │
1141
+ │ │ ┌──────────────────────────▼──────────────────────────────────┐ │ │
1142
+ │ │ │ Firecracker (aarch64) │ │ │
1143
+ │ │ │ │ │ │
1144
+ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │
1145
+ │ │ │ │ MicroVM │ │ MicroVM │ │ MicroVM │ │ │ │
1146
+ │ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │
1147
+ │ │ └──────────────────────────────────────────────────────────────┘ │ │
1148
+ │ └─────────────────────────────────────────────────────────────────────┘ │
1149
+ │ │
1150
+ └──────────────────────────────────────────────────────────────────────────────┘
1151
+ ```
1152
+
1153
+ ### Lima Setup
1154
+
1155
+ ```bash
1156
+ # Create Lima instance with nested virtualization
1157
+ limactl create \
1158
+ --name=gitspace \
1159
+ --vm-type=vz \
1160
+ --mount-type=virtiofs \
1161
+ --set '.nestedVirtualization = true' \
1162
+ template://ubuntu
1163
+
1164
+ # Start it
1165
+ limactl start gitspace
1166
+
1167
+ # Shell in
1168
+ limactl shell gitspace
1169
+
1170
+ # Inside Lima: Install Firecracker (aarch64)
1171
+ curl -L https://github.com/firecracker-microvm/firecracker/releases/download/v1.5.0/firecracker-v1.5.0-aarch64.tgz | tar -xz
1172
+ sudo mv release-*/firecracker /usr/local/bin/
1173
+
1174
+ # Verify KVM works
1175
+ ls -la /dev/kvm
1176
+ ```
1177
+
1178
+ ### Lima Configuration File
1179
+
1180
+ ```yaml
1181
+ # ~/.lima/gitspace/lima.yaml
1182
+
1183
+ vmType: vz
1184
+ arch: aarch64
1185
+ cpus: 4
1186
+ memory: 8GiB
1187
+ disk: 100GiB
1188
+
1189
+ nestedVirtualization: true
1190
+
1191
+ mounts:
1192
+ - location: "~"
1193
+ writable: true
1194
+ - location: "/tmp/lima"
1195
+ writable: true
1196
+
1197
+ provision:
1198
+ - mode: system
1199
+ script: |
1200
+ #!/bin/bash
1201
+ apt-get update
1202
+ apt-get install -y containerd
1203
+
1204
+ # Install Firecracker
1205
+ curl -L https://github.com/firecracker-microvm/firecracker/releases/download/v1.5.0/firecracker-v1.5.0-aarch64.tgz | tar -xz -C /usr/local/bin
1206
+
1207
+ # Install Flintlock
1208
+ curl -L https://github.com/liquidmetal-dev/flintlock/releases/download/v0.9.0/flintlock_0.9.0_linux_arm64.tar.gz | tar -xz -C /usr/local/bin
1209
+ ```
1210
+
1211
+ ### Parity Matrix
1212
+
1213
+ | Component | Mac (Lima) | GCP (Spot VM) |
1214
+ |-----------|------------|---------------|
1215
+ | Outer VM | Lima (Virtualization.framework) | GCE (KVM) |
1216
+ | Inner VM | Firecracker (aarch64) | Firecracker (x86_64) |
1217
+ | Image format | Same OCI image | Same OCI image |
1218
+ | Storage | Local SSD | PD-SSD + btrfs |
1219
+ | Networking | Lima bridge | GCP VPC + NAT |
1220
+
1221
+ **Key insight:** Same OCI images, same Firecracker, same gitspace experience. Only the architecture (arm64 vs x86_64) and outer layer differ.
1222
+
1223
+ ---
1224
+
1225
+ ## Components to Build
1226
+
1227
+ ### Option A: DIY Orchestration
1228
+
1229
+ | Component | Description | Effort |
1230
+ |-----------|-------------|--------|
1231
+ | **gitspace-relay** | WebSocket relay, auth, GitHub hooks | High |
1232
+ | **gitspace-daemon** | Runs on each host, manages VMs | High |
1233
+ | **Custom scheduler** | Track hosts, schedule gitspaces | Medium |
1234
+ | **Volume manager** | btrfs subvolumes, quotas | Medium |
1235
+ | **Tunnel manager** | Cloudflare tunnel per gitspace | Low |
1236
+ | **Image builder** | BuildKit pipeline | Medium |
1237
+ | **CLI** | User-facing commands | Medium |
1238
+ | **Web UI** | Dashboard (optional) | High |
1239
+
1240
+ **Total: 6-8 significant components**
1241
+
1242
+ ### Option B: Flintlock + Nomad
1243
+
1244
+ | Component | Description | Effort |
1245
+ |-----------|-------------|--------|
1246
+ | **gitspace-relay** | WebSocket relay, auth, GitHub hooks, Nomad job submission | High |
1247
+ | **Nomad job templates** | HCL templates for gitspaces | Low |
1248
+ | **Volume manager** | btrfs subvolumes, quotas | Medium |
1249
+ | **Tunnel manager** | Cloudflare tunnel per gitspace | Low |
1250
+ | **Image builder** | BuildKit pipeline (OCI for Flintlock) | Medium |
1251
+ | **Host provisioner** | GCP API for scale up/down | Medium |
1252
+ | **CLI** | User-facing commands | Medium |
1253
+
1254
+ **Uses:** Nomad (scheduling), Flintlock (VM lifecycle), containerd (images)
1255
+
1256
+ **Total: 5-6 components, leveraging battle-tested tools**
1257
+
1258
+ ### Option C: Nomad Only
1259
+
1260
+ | Component | Description | Effort |
1261
+ |-----------|-------------|--------|
1262
+ | **gitspace-relay** | WebSocket relay, auth, Nomad job submission | High |
1263
+ | **gitspace-daemon** | Lighter version, manages FC directly | Medium |
1264
+ | **Nomad job templates** | HCL with raw_exec for Firecracker | Medium |
1265
+ | **Volume manager** | btrfs subvolumes, quotas | Medium |
1266
+ | **Tunnel manager** | Cloudflare tunnel per gitspace | Low |
1267
+ | **Image builder** | BuildKit + manual rootfs conversion | Medium |
1268
+ | **Host provisioner** | GCP API for scale up/down | Medium |
1269
+ | **CLI** | User-facing commands | Medium |
1270
+
1271
+ **Total: 6-7 components**
1272
+
1273
+ ---
1274
+
1275
+ ## Decision Matrix
1276
+
1277
+ | Factor | DIY | Flintlock + Nomad | Nomad Only |
1278
+ |--------|-----|-------------------|------------|
1279
+ | **Complexity** | High | Medium | Medium |
1280
+ | **Dependencies** | Few | Nomad, Flintlock, containerd | Nomad |
1281
+ | **Scheduling** | Build it | Nomad (proven) | Nomad (proven) |
1282
+ | **VM lifecycle** | Build it | Flintlock (clean API) | Build it |
1283
+ | **Image management** | Manual | containerd (OCI native) | Manual |
1284
+ | **Scale to zero** | Build it | Build it | Build it |
1285
+ | **Spot handling** | Build it | Nomad drain + build | Nomad drain + build |
1286
+ | **Learning curve** | Low | Medium | Low-Medium |
1287
+ | **Control** | Full | High | High |
1288
+ | **Fun** | Most | Some | Some |
1289
+
1290
+ ### Recommendation
1291
+
1292
+ **Start with:** Flintlock + Nomad
1293
+
1294
+ **Why:**
1295
+ 1. Nomad's scheduling is non-trivial to build correctly
1296
+ 2. Flintlock's OCI integration saves significant work
1297
+ 3. Both are well-documented with active communities
1298
+ 4. Can always replace components later
1299
+ 5. Focus engineering effort on the unique parts (relay, CLI, user experience)
1300
+
1301
+ **Progression:**
1302
+ 1. Single host + Flintlock (no Nomad) for initial development
1303
+ 2. Add Nomad when multi-host is needed
1304
+ 3. GCP Spot integration for cost optimization
1305
+ 4. Scale based on demand
1306
+
1307
+ ---
1308
+
1309
+ ## Summary
1310
+
1311
+ ```
1312
+ ┌─────────────────────────────────────────────────────────────────────────────┐
1313
+ │ GITSPACE INFRASTRUCTURE │
1314
+ │ │
1315
+ │ Control Plane │
1316
+ │ ├── gitspace.sh relay (auth, WebSocket, GitHub, scheduling) │
1317
+ │ └── Nomad Server (job scheduling, cluster state) │
1318
+ │ │
1319
+ │ Data Plane (per host) │
1320
+ │ ├── Nomad Client (receives jobs) │
1321
+ │ ├── Flintlock (manages Firecracker VMs) │
1322
+ │ ├── containerd (OCI images) │
1323
+ │ ├── cloudflared (preview tunnels) │
1324
+ │ └── Firecracker (microVMs) │
1325
+ │ │
1326
+ │ Storage │
1327
+ │ ├── OCI images from registry (kernel, rootfs) │
1328
+ │ └── btrfs volumes (persistent workspaces) │
1329
+ │ │
1330
+ │ Networking │
1331
+ │ ├── Terminal: WebSocket via relay (E2E encrypted) │
1332
+ │ ├── Preview: Cloudflare Tunnel (HTTPS) │
1333
+ │ └── Outbound: NAT via host │
1334
+ │ │
1335
+ │ Hosts │
1336
+ │ ├── GCP Spot VMs (scale 0 to N, 70% cheaper) │
1337
+ │ ├── GCP On-Demand (reliable fallback) │
1338
+ │ ├── Latitude.sh Bare Metal (dedicated, hourly) │
1339
+ │ ├── Self-hosted (your computer) │
1340
+ │ └── Mac + Lima (local development) │
1341
+ │ │
1342
+ └─────────────────────────────────────────────────────────────────────────────┘
1343
+ ```
1344
+
1345
+ ---
1346
+
1347
+ *Last updated: 2024-12*