also looking at this
fix(deploy): keep SQLite across deploys, and preflight an attached host's services
#174
16 files
+916
-12
| @@ -153,6 +153,36 @@ is often worth making, but it should be a decision rather than a surprise. | ||
| 153 | 153 | token actually reaches, separating the owner's boxes from the ones no one asked |
| 154 | 154 | for, so the radius can be shown before an attach is approved. |
| 155 | 155 | |
| 156 | ### It cannot install services on the owner's box | |
| 157 | ||
| 158 | Attach mode provisions nothing — installing engines on someone else's server is | |
| 159 | not a tenant's business. So `infrastructure.compute.managedServices` in an | |
| 160 | attached config is a statement about what the HOST already runs, not a request | |
| 161 | to install anything: | |
| 162 | ||
| 163 | ```typescript | |
| 164 | cloud: { provider: 'hetzner', attachTo: 'statushq' }, | |
| 165 | infrastructure: { | |
| 166 | appDatabase: { engine: 'postgres', name: 'loghq', username: 'loghq', password: '…' }, | |
| 167 | compute: { managedServices: { postgres: true } }, // statushq must ALREADY run postgres | |
| 168 | } | |
| 169 | ``` | |
| 170 | ||
| 171 | If the owner's box runs SQLite, there is no Postgres for the tenant's role and | |
| 172 | database to be created in, and nothing will ever install one. The deploy checks | |
| 173 | this before it ships anything and stops with the incompatibility named: | |
| 174 | ||
| 175 | ``` | |
| 176 | This project declares managedServices.postgres, but 'statushq' has no postgres | |
| 177 | on 203.0.113.10. Attach mode does not provision services on the owner's box, so | |
| 178 | nothing will install it. Point the app at an external service, or ask the owner | |
| 179 | of 'statushq' to add it to their own config and re-provision. | |
| 180 | ``` | |
| 181 | ||
| 182 | A service counts as present when its binary is on `PATH` or something is | |
| 183 | listening on its port. If the check itself cannot get an answer, the deploy | |
| 184 | proceeds — it is a preflight, not a gate. | |
| 185 | ||
| 156 | 186 | ## Two app models |
| 157 | 187 | |
| 158 | 188 | ts-cloud deploys apps two ways; pick per environment: |
| @@ -344,6 +374,54 @@ clean. If a site targets a server (`deploy: 'server'`, or `start` set) but no | ||
| 344 | 374 | actionable error instead of failing silently at runtime — set `deploy: 'bucket'` |
| 345 | 375 | or add a server. |
| 346 | 376 | |
| 377 | ### State that must survive a deploy | |
| 378 | ||
| 379 | Each deploy unpacks into a NEW `releases/<id>` directory and flips `current` at | |
| 380 | it; old releases are pruned. Anything the app writes and must keep therefore has | |
| 381 | to live in the site's `shared/` directory and be symlinked in, which is what | |
| 382 | `site.sharedPaths` declares. `.env` is always shared. | |
| 383 | ||
| 384 | ```typescript | |
| 385 | sites: { | |
| 386 | app: { | |
| 387 | start: 'bun run server.ts', | |
| 388 | sharedPaths: ['storage', 'public/uploads'], | |
| 389 | }, | |
| 390 | } | |
| 391 | ``` | |
| 392 | ||
| 393 | A **SQLite database is shared automatically**. The deploy already knows the | |
| 394 | connection and the file path from the environment it writes to the box, so when | |
| 395 | `DB_CONNECTION` is `sqlite` and `DB_DATABASE` names a path inside the release, | |
| 396 | that file is added to `sharedPaths` for you and the deploy log says so. Without | |
| 397 | it the database sits inside a release directory and the next deploy starts the | |
| 398 | app on an empty one — silently, with the data still in a release that is about | |
| 399 | to be pruned. | |
| 400 | ||
| 401 | Two cases it does not cover: | |
| 402 | ||
| 403 | - **`DB_DATABASE` unset.** An app can default its own path internally, which the | |
| 404 | deploy never sees. Guessing the filename would report the data as safe while | |
| 405 | sharing a path the app may not use, so the deploy warns instead — set | |
| 406 | `DB_DATABASE`, or list the file in `sharedPaths` yourself. | |
| 407 | - **An absolute path.** A database outside the release tree already survives; a | |
| 408 | deploy replaces the release, not the filesystem around it. | |
| 409 | ||
| 410 | Turning existing on-box state into shared state does not throw it away: the | |
| 411 | first deploy to share a path copies the live release's copy into `shared/` | |
| 412 | (SQLite's `-wal`/`-shm` sidecars included), and a site's first deploy seeds a | |
| 413 | still-empty shared file from the copy the artifact shipped. | |
| 414 | ||
| 415 | Several sites of one project can share ONE file — an app and its API on one | |
| 416 | SQLite database — with the object form, which names an absolute `target`: | |
| 417 | ||
| 418 | ```typescript | |
| 419 | sharedPaths: [{ path: 'database/app.sqlite', target: '/var/www/acme-app/shared/database/app.sqlite', seed: false }] | |
| 420 | ``` | |
| 421 | ||
| 422 | Each site installs under its own base, so a plain string would give each of them | |
| 423 | a database of its own. `seed: false` marks the sites that do not own the file. | |
| 424 | ||
| 347 | 425 | ### CDN / caching |
| 348 | 426 | |
| 349 | 427 | The `cache` hint applies to either origin: |