ReviewOS

also looking at this

stacks/ts-cloud

fix(deploy): keep SQLite across deploys, and preflight an attached host's services

#174
Merged glennmichael123 wants to merge fix/sqlite-shared-path-and-attach-preflight into main
16 files +916 -12
docs/config.mdmodified+78-0
Changes to docs/config.md
@@ -153,6 +153,36 @@ is often worth making, but it should be a decision rather than a surprise.
153153token actually reaches, separating the owner's boxes from the ones no one asked
154154for, so the radius can be shown before an attach is approved.
155155
156### It cannot install services on the owner's box
157
158Attach mode provisions nothing installing engines on someone else's server is
159not a tenant's business. So `infrastructure.compute.managedServices` in an
160attached config is a statement about what the HOST already runs, not a request
161to install anything:
162
163```typescript
164cloud: { provider: 'hetzner', attachTo: 'statushq' },
165infrastructure: {
166 appDatabase: { engine: 'postgres', name: 'loghq', username: 'loghq', password: '…' },
167 compute: { managedServices: { postgres: true } }, // statushq must ALREADY run postgres
168}
169```
170
171If the owner's box runs SQLite, there is no Postgres for the tenant's role and
172database to be created in, and nothing will ever install one. The deploy checks
173this before it ships anything and stops with the incompatibility named:
174
175```
176This project declares managedServices.postgres, but 'statushq' has no postgres
177on 203.0.113.10. Attach mode does not provision services on the owner's box, so
178nothing will install it. Point the app at an external service, or ask the owner
179of 'statushq' to add it to their own config and re-provision.
180```
181
182A service counts as present when its binary is on `PATH` or something is
183listening on its port. If the check itself cannot get an answer, the deploy
184proceeds it is a preflight, not a gate.
185
156186## Two app models
157187
158188ts-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
344374actionable error instead of failing silently at runtime set `deploy: 'bucket'`
345375or add a server.
346376
377### State that must survive a deploy
378
379Each deploy unpacks into a NEW `releases/<id>` directory and flips `current` at
380it; old releases are pruned. Anything the app writes and must keep therefore has
381to 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
385sites: {
386 app: {
387 start: 'bun run server.ts',
388 sharedPaths: ['storage', 'public/uploads'],
389 },
390}
391```
392
393A **SQLite database is shared automatically**. The deploy already knows the
394connection 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,
396that file is added to `sharedPaths` for you and the deploy log says so. Without
397it the database sits inside a release directory and the next deploy starts the
398app on an empty one silently, with the data still in a release that is about
399to be pruned.
400
401Two 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
410Turning existing on-box state into shared state does not throw it away: the
411first 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
413still-empty shared file from the copy the artifact shipped.
414
415Several sites of one project can share ONE file an app and its API on one
416SQLite database with the object form, which names an absolute `target`:
417
418```typescript
419sharedPaths: [{ path: 'database/app.sqlite', target: '/var/www/acme-app/shared/database/app.sqlite', seed: false }]
420```
421
422Each site installs under its own base, so a plain string would give each of them
423a database of its own. `seed: false` marks the sites that do not own the file.
424
347425### CDN / caching
348426
349427The `cache` hint applies to either origin: