Loomup Docs
Guide /docs/deployment

Deployment

Loomup is a single static binary plus a SQLite file. No Docker, Redis, or Postgres is required for MVP.

For a complete two-service walkthrough using the runnable Astro example, see Deploy the Astro + Loomup example.

For a backend-only DigitalOcean Droplet deployment with GitHub Actions, see Deploy Loomup to a DigitalOcean VPS.

Multi-tenant control plane (optional)

For managing many isolated projects from one host, use the control plane catalog (not the same as tenant app DBs):

bash
loomup platform register --email ops@example.com --password '…'
export LOOMUP_PLATFORM_TOKEN=…
loomup platform create-project --name app --admin-password '…'
loomup platform status
loomup platform backup-catalog -o /var/backups/loomup-control.sqlite
# Serve a provisioned project:
loomup serve --config .loomup/projects/<id>/loomup.toml

Defaults: catalog .loomup/control.sqlite, project roots under .loomup/projects/. See control-plane.md.

Install / binary distribution

Build from source

bash
cargo build --release
# binary: target/release/loomup

Copy target/release/loomup to the host (or keep it in your deploy artifact). Place it on PATH (e.g. /usr/local/bin/loomup).

Prebuilt binary (release artifacts)

Tag a version (git tag v0.x.y && git push origin v0.x.y) to run .github/workflows/release.yml. The release workflow first runs the server Core CI quality gate, then builds Linux/macOS/Windows binaries and attaches them to a GitHub Release. The crates.io publish job runs only after that gate succeeds. SDK and npm releases are owned by bluppco/loomup-js.

Download the asset for your OS/arch, extract if needed, chmod +x, and install onto PATH:

bash
# Example after downloading loomup-*-apple-darwin.tar.gz:
tar xzf loomup-aarch64-apple-darwin.tar.gz
install -m 755 ./loomup-aarch64-apple-darwin /usr/local/bin/loomup
loomup --version

Container (optional)

A minimal runtime image can package the release binary:

dockerfile
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
  && rm -rf /var/lib/apt/lists/*
COPY loomup /usr/local/bin/loomup
WORKDIR /data
EXPOSE 3000
ENTRYPOINT ["loomup"]
CMD ["serve", "--config", "/data/loomup.toml"]

Build the binary with cargo build --release, copy target/release/loomup into the image context, then docker build -t loomup .. Mount a volume for the project directory (config + SQLite).

Project layout

On the server (or locally):

bash
loomup init /var/lib/loomup/app \
  --admin-email admin@example.com \
  --admin-password 'change-me-now'
cd /var/lib/loomup/app

--admin-email and --admin-password are required (no default admin is seeded).

This creates:

  • loomup.toml — bind address, DB path, auth, table rules (mode 0600 on Unix; contains a project-local JWT secret)
  • data/app.sqlite — portable SQLite database (WAL by default)
  • When object storage is enabled: data/storage/ (or storage.root) holds file bytes alongside the DB

Re-running init on an existing project reuses database.path from the existing loomup.toml (it does not force data/app.sqlite).

Configuration

Edit loomup.toml:

toml
[server]
host = "0.0.0.0"
port = 3000
# Required for public binds: TLS at the reverse proxy, or explicit insecure opt-in.
behind_tls_proxy = true
# allow_insecure_public_bind = true  # only if you intentionally expose plaintext HTTP
trust_proxy = true  # only when the proxy overwrites X-Forwarded-For

[database]
path = "./data/app.sqlite"
wal = true
# pool_size = 8
# Free-tier default (1 GiB). Set 0 for unlimited.
max_size_bytes = 1073741824
# Optional: second SQLITE_OPEN_READONLY pool on the same file (no lag).
# Speeds CRUD GETs when the write pool is busy.
# read_pool_size = 16
# Optional: external replica DB files (LiteFS / Litestream / operator copy).
# CRUD GETs round-robin across these; writes always hit `path`.
# May return stale data under replication lag — leave empty for strong consistency.
# read_replicas = ["./data/app-replica.sqlite"]

[auth]
jwt_secret_env = "LOOMUP_JWT_SECRET"
# cookie_mode = true   # optional Secure HttpOnly cookies for browser apps
# cookie_secure = true

Read pools and replicas

SettingBehavior
(default)All traffic uses the primary pool on database.path
read_pool_size > 0, empty read_replicasSame-file read-only pool for GET /api/... and admin record list
read_replicas = [...]One read-only pool per path; GETs round-robin. Per-replica pool size is read_pool_size if set, else pool_size

Writes, auth, CDC, migrations, and realtime revalidation always use the primary. /health and /ready gate on the primary only.

Do not list database.path inside read_replicas — use read_pool_size for a same-file read pool. Relative replica paths resolve against the config file directory (like database.path).

Free-tier size quotas

ConfigDefaultNotes
database.max_size_bytes1 GiBPayload size (page_count − freelist_count) × page_size. 0 = unlimited.
storage.max_total_bytes100 MiBSum of object sizes across buckets. 0 = unlimited.
storage.max_upload_bytes50 MiBPer-object upload body cap (must be ≤ max_total_bytes when total is set).

Growth writes that would exceed DB/storage quotas return HTTP 507 (database_quota_exceeded / storage_quota_exceeded). Deletes free DB freelist quota and storage object bytes. Raise limits for paid tiers or larger local datasets.

Defaults do not override values already saved in a project's loomup.toml. Projects created with the former 5 MiB database default can retain max_size_bytes = 5242880 after a backend upgrade. To raise those projects to 1 GiB, back up each affected config, set [database].max_size_bytes to 1073741824, and reload the project runtime or restart its host so the in-memory configuration is refreshed. Preserve intentional custom limits and 0 (unlimited).

Required for production: set a strong JWT secret:

bash
export LOOMUP_JWT_SECRET="$(openssl rand -hex 32)"

If the env var is unset, Loomup uses auth.dev_secret only when auth.allow_insecure_dev_secret = true (set by init for local quickstart with a unique generated secret). Production must set the env var and disable the insecure fallback.

Run

bash
export LOOMUP_JWT_SECRET=...
loomup serve --config /var/lib/loomup/app/loomup.toml

Endpoints:

PathPurpose
GET /healthLiveness (process up, DB open)
GET /readyReadiness: DB reachable and no pending application migrations
http://HOST:PORT/adminAdmin UI
http://HOST:PORT/api/{table}REST CRUD
ws://HOST:PORT/realtimeWebSocket subscriptions

Put a reverse proxy (Caddy, nginx, Traefik) in front for TLS. Forward WebSockets for /realtime. In the permanent blue/green host, a committed cutover closes sockets on the retiring generation with 1012 service restart; supported SDKs reconnect with jitter, re-subscribe, and resynchronize against the already-active slot.

Auth rate limits behind a reverse proxy

By default Loomup rate-limits by TCP peer address. Behind Caddy/nginx every browser shares the proxy’s IP, so all clients collapse into one bucket.

Set in loomup.toml:

toml
[server]
trust_proxy = true

When trust_proxy = true, the limiter uses the left-most X-Forwarded-For hop (or X-Real-IP). Only enable this when the proxy overwrites those headers so clients cannot spoof identity.

Request ids and access logs

Every HTTP response includes an x-request-id header:

  • If the client sent x-request-id, that value is propagated (not overwritten).
  • Otherwise the server generates a UUID.

Browser clients can send and read the header (CORS allows and exposes x-request-id).

One structured request completion log (method, normalized route, status, latency, request id) is emitted at info. Successful /health, /ready, and internal slot-health probes are omitted; failed probes remain logged. Hosted project requests are logged once by their project router. Request start/finish diagnostics are available at debug via tower_http. Logs go to stderr. Tune with RUST_LOG, for example:

bash
RUST_LOG=info loomup serve --config loomup.toml
# or quieter / louder:
RUST_LOG=info,tower_http=debug loomup serve --config loomup.toml

Diagnosing slow requests

Responses passing through request observability include a standard Server-Timing header, in milliseconds, regardless of log level. For example (illustrative values):

http
Server-Timing: loomup_project_total;dur=4.200, loomup_project_db_pool;dur=0.100, loomup_project_db_work;dur=1.800
Server-Timing: loomup_platform_total;dur=5.100, loomup_platform_project_router;dur=4.300

loomup_platform_total is the outer application duration for hosted requests; loomup_project_total is the project duration (and the application total for a standalone server). Each observed stage below is prefixed with its scope: loomup_project_ or loomup_platform_. The body metric is elapsed read time. Unused stages are omitted. Only fixed names and durations are exposed, without request metadata, SQL, object keys, or error details. Health/readiness/slot probes return totals only and retain their existing log exclusions. CORS exposes the header to browser fetch clients; CORS preflight may return before observability. Cross-origin Resource Timing access is not enabled by this change.

These are overlapping measurements, not additive slices. The platform total includes project execution, blocking work includes nested database/storage work, and aggregated concurrent operations may exceed wall time. Totals stop when the response is prepared, excluding response-body delivery and network transit. A cache can replay an earlier response's timings; check cache status when profiling.

To split a client's IPv4/IPv6 elapsed time, collect each request's header alongside curl's cumulative connection timestamps (repeat with -6 for IPv6):

bash
curl -4 -sS -D - -o /dev/null \
  -w '\nDNS=%{time_namelookup} TCP=%{time_connect} TLS=%{time_appconnect} READY=%{time_pretransfer} FIRST_BYTE=%{time_starttransfer} TOTAL=%{time_total}\n' \
  'https://tryloomup.com/health'

curl reports seconds. For a fresh direct HTTPS connection without redirects: DNS = DNS; TCP setup = TCP - DNS; TLS = TLS - TCP; remaining setup = READY - TLS; request-to-first-byte = FIRST_BYTE - READY; response transfer = TOTAL - FIRST_BYTE. Request-to-first-byte includes request upload, transit, proxy/CDN work, and origin processing. Subtracting the outer Loomup total gives only an approximate residual, not a pure network measurement. These formulas need adjustment for reused connections, proxies, HTTP/3, or redirects. Measure the same route, method, payload and authentication as the workload of interest; a health request only measures the health path. Calculate splits per request before summarizing them; subtracting independent medians is not a valid split. See the curl timing reference and Server Timing specification.

The server records bounded stage timings independently of sampled SQL diagnostics. No enablement flag is needed. http.request.diagnostics is emitted at info when response preparation takes at least one second or returns HTTP 5xx; faster responses and http.request.started are debug. Requests still running emit http.request.slow at warn every ten seconds. Dropping the request future before a response produces http.request.cancelled; this means cancellation, not proof that the client disconnected. Health/readiness/slot probes retain their existing completion logging but do not produce stage diagnostics.

Events carry the existing request ID, normalized route, project ID, and a bounded cf_ray value when supplied. cf_ray is correlation metadata, not verified identity. The timings field is a JSON-encoded object with these measurements:

MeasurementWhat it covers
bodyBytes read, state, first poll and finish offsets, and elapsed time from first poll to completion/error/drop. Includes consumer scheduling and backpressure, not just network transfer. The wrapper preserves streaming, trailers, errors and endpoint body limits.
blocking_queueTime from submitting work through the server's blocking helper until a blocking thread begins it.
blocking_workTime executing that closure, including any nested DB/R2 work. A completed closure can still return an application error.
db_poolConnection checkout or schema-connection mutex wait in with_conn / with_conn_mut.
db_workTime inside those connection closures, including SQL, locks and other closure work; not SQL execution alone.
r2_head, r2_get, r2_put, r2_deleteObject-store operation elapsed time, including library retries/backoff. Buffered GET includes reading the object body.
r2_multipartMultipart initialization, file reading, parts, completion and any abort cleanup.
r2_get_headersStreaming download GET until object response headers; excludes subsequent response-body streaming.
project_routerTime inside a project router, recorded by its surrounding platform trace.
authenticationProject JWT validation and current-user lookup, service-key lookup, password authentication, and platform session lookup. Includes repeated checks and any nested DB work.
authorizationResource/storage scope checks, named-operation/job access checks, row-level rule evaluation (including relationship queries), and platform workspace membership checks. This is the instrumented subset of authorization, not every inline permission branch.
usage_meteringHTTP usage middleware preparation/completion and row-meter middleware preparation/completion, including awaited receipt bookkeeping. Excludes the wrapped handler, background export/flush, and response-stream accounting that happens after headers are prepared.
serializationProject/platform JSON response encoding and row-meter JSON re-encoding after adding usage metadata. Excludes earlier construction of JSON values, request decoding, compression and network delivery.
usage_begin_transactionRow-journal transaction recording the request attempt: transaction start, SQL and commit. Starts after the journal mutex is acquired and its connection is initialized.
usage_completion_transactionRow-journal transaction persisting the receipt, updating aggregates and marking completion, including commit. Excludes journal mutex wait and receipt JSON encoding. SQLite lock/busy waits within either transaction are included.
usage_begin_sql, usage_completion_sqlTransaction creation and all pre-commit work in the respective journal transaction: SQL preparation/execution, lookups, inserts and aggregate/completion updates. Includes SQLite lock/busy waits encountered there and rollback cleanup on a SQL failure; excludes the explicit commit call, journal mutex wait and receipt JSON encoding.
usage_begin_commit, usage_completion_commitThe respective Transaction::commit() call, including any SQLite synchronization, checkpoint, lock wait or error cleanup it performs. This is not a pure disk-sync measurement. Omitted when pre-commit SQL fails and commit is never attempted.
usage_queue_waitSubmission-to-start delay for the row-meter middleware's blocking receipt jobs. Excludes job execution and the async task's resumption after the job returns. Separate from the generic blocking_queue metric.
usage_jsonRow receipt JSON encoding, usage metadata construction, and response JSON parsing, metadata insertion and re-encoding. Excludes response-body collection and waiting for receipt work. Response re-encoding also appears in serialization.
usage_journal_waitWaiting for the row-journal connection mutex. Covers request-attributed journal accesses, including registration during handler execution; excludes transaction work and connection initialization.

The metering detail metrics retain the loomup_project_ / loomup_platform_ prefixes and accompany the existing usage_metering total. They are not an additive set: each transaction total includes its SQL and commit timings. Do not add those child timings to their parent total. They are also not an exhaustive additive partition: identity checks, bookkeeping and async resumption can remain outside the detail metrics; journal access from inside a handler can fall outside the middleware total. These metrics are independent of SQL sampling. Receipt workers carry timing context only; their journal SQL is not billed as customer row work. Background recovery/export has no request attribution.

These four stages use the same scope prefixes in Server-Timing, for example loomup_project_authentication;dur=1.250. Metering includes its authentication checks and row-meter response re-encoding; authorization can run inside DB work. Do not add these overlapping durations. Counts describe measured calls, not successful logins or allowed access; a denied boolean rule still completed its evaluation. Missing stages mean the instrumented code was not executed, not that the entire category took zero time. Neither headers nor logs expose credentials, rule text, SQL, or response contents.

Each stage reports active, completed, failed and cancelled operation counts, aggregate total_ms (including active work), and max_finished_ms for operations that have ended, including errors/cancellation. Unused stages are omitted. Timings overlap: do not sum them. R2 timings do not expose individual retry attempts or separate DNS/TCP/TLS phases. Request summaries end when the response is ready, not when every response byte reaches the client.

Hosted requests have request_scope=platform for admission/routing and request_scope=project for the project handler. Match their request IDs; the platform scope uses project ID __platform__ because admission precedes project resolution. Each scope has its own timings and body counters (do not add those counters for billing). Only the existing completion event remains deduplicated. A platform slow event with no active project_router stage points to time outside the project handler. An active stage identifies outstanding work, not proof of its root cause. The diagnostic timer also depends on Tokio scheduling; a completely blocked runtime cannot emit timely progress events.

For a temporary detailed trace:

bash
RUST_LOG=info,loomup::request_diagnostics=debug loomup serve --config loomup.toml

Correlate cf_ray and request ID with reverse-proxy/Cloudflare logs. Failed TCP or TLS handshakes that never reach the application cannot appear in these request traces; investigate them at the proxy/network layer. These logs exclude raw URLs, query strings, SQL, object keys, request bodies and error messages.

System schema / CDC

After schema changes or first deploy:

bash
loomup migrate --config loomup.toml
loomup cdc install --config loomup.toml
loomup cdc status --config loomup.toml

serve also bootstraps auth + CDC on startup.

Backups

SQLite is a file — back it up regularly:

bash
loomup backup --config loomup.toml --output /backups/app-$(date +%F).sqlite

Prefer stopping writers or using the CLI backup (online SQLite backup API) rather than copying a live file while under heavy write load.

If object storage is enabled (storage.enabled = true), also back up storage.root (default ./data/storage). The SQLite backup does not include file bytes. See storage.md.

Users

bash
loomup admin create-user --email you@example.com --password '...' --role admin

Disable users from the Admin UI (Users tab) or PATCH /admin/api/users/{id} with { "disabled": true }.

TypeScript types

bash
loomup gen typescript --config loomup.toml --output ./src/loomup-types.ts

Process supervision

Use systemd, launchd, or your process manager. Example unit sketch:

ini
[Service]
Environment=LOOMUP_JWT_SECRET=...
WorkingDirectory=/var/lib/loomup/app
ExecStart=/usr/local/bin/loomup serve --config /var/lib/loomup/app/loomup.toml
Restart=on-failure

Security checklist

  • Strong LOOMUP_JWT_SECRET
  • Bind to localhost if behind a proxy, or firewall port 3000
  • HTTPS at the proxy
  • Restrict admin accounts; disable unused users
  • Table rules deny by default when not configured — set explicit rules in TOML or Admin → Rules
  • Keep the SQLite file permissions tight (chmod 600)

TLS / WSS

Loomup binds plaintext HTTP/WS. For production:

  1. Terminate TLS at a reverse proxy (nginx, Caddy, Traefik).
  2. Set server.behind_tls_proxy = true when binding a public interface, or
  3. Set server.allow_insecure_public_bind = true only for explicit insecure demos.

Public binds to 0.0.0.0 / :: refuse to start without one of those flags.

Clients should use https:// / wss:// URLs through the proxy.

toml
[auth]
cookie_mode = true
cookie_secure = true   # false only for local HTTP

Login/register/refresh set HttpOnly cookies (loomup_access, loomup_refresh) in addition to the JSON body.

Health path privacy

toml
[admin]
protect_health = true

When true, /health and /ready require an admin Bearer token so path/DB details are not public.

Scoped handles

Loomup can allocate canonical handles on membership CRUD inserts. Declare a required text handle field and a unique index on [workspace_id, handle], then opt in with:

yaml
$handles:
  workspace_memberships:
    field: handle
    scope: workspace_id
    source: user_id.email

The source follows one declared foreign key to a required text email field. Handle resources require a single id primary key. Omitted or null handles on CRUD insertion use the lowercase email prefix (unsupported character runs become hyphens), or member for bases shorter than three characters. Allocation tries the base, then numeric suffixes 1–9999, truncating to 30 characters. Selection and insertion share the write transaction. REST, sync, admin and Studio use this path. Explicit values normalize whitespace/case but are never automatically suffixed. Updates do not regenerate handles when other fields change.

Handles contain 3–30 lowercase ASCII letters, digits, hyphens or underscores and start/end with a letter or digit. Invalid edits return 400 invalid_handle; collisions return 409 handle_taken; exhausted allocation returns 409 handle_allocation_failed. Database triggers and the scoped unique index protect direct SQL writes, which must supply valid handles themselves. Applying an unchanged schema refreshes handle guards, including fixes shipped by a newer backend; it validates existing handles before replacing the triggers.

For existing tables first add a nullable handle column, enable the policy, and backfill existing rows before making the column required. Assigned handles cannot be cleared. Retain existing handles when rerunning a backfill. Schema plans must be reviewed before each apply; clients must not invent fallback handles.

Hosted asset CDN

The independent base/cdn/ repository owns the Cloudflare Worker, pinned npm tooling, runtime tests, R2 binding configuration, and CDN deployment workflow. This repository owns its authorization and usage-ingestion endpoints.

Hosted environment settings (loaded on startup):

SettingMeaning
LOOMUP_CDN_BASE_URLHTTPS origin, normally https://cdn.tryloomup.com; unset disables integration.
LOOMUP_CDN_PROJECT_IDSComma-separated project allowlist; * enables delivery for all; empty issues no new redirects.
LOOMUP_CDN_SERVICE_TOKENShared operator secret, at least 32 visible ASCII characters; never a user JWT or project key.

CDN requires R2 and an isolated projects/<project-id> prefix. Supply the first two settings through backend GitHub environment variables and the service token through a secret. The DigitalOcean workflow installs them in the existing restricted environment file. Do not place the token in tenant TOML or client code.

Deploy backend support disabled, release the compatible SDK, deploy the Worker from its own repository, then enable one test project. Its repository README describes queue provisioning, required Cloudflare credentials, and the dry run. Verify public/private files, issuer revocation, deletion/overwrite, browser redirects, usage reconciliation, and outbound bandwidth before expanding.

For rollback, empty the project allowlist and redeploy. Keep the base URL, service token, Worker, and queue consumer alive for at least 24 hours and until usage is drained. Removing them immediately breaks outstanding version-2 links. Rotate the service credential in coordination across the two deployments.

Every CDN response rechecks server authorization; origin outages return errors rather than cached private bytes. Do not configure a cache in front of the Worker that bypasses execution, and do not expose its physical R2 bucket publicly.

Schema isolation rollout and deployment fences

After schema workers drain, deployment records the serving project's health baseline, cross-checking the read-only catalog and durable markers. Older servers can omit unloaded projects from status; only catalog projects with a valid existing quarantine marker may fill those gaps. A missing project without that marker blocks deployment. A project already fenced by a durable schema quarantine may remain quarantined in the candidate so recovery fixes can deploy. The candidate must retain the same quarantine operation and report schema recovery required. New failures, missing projects, and an unhealthy default project still block deployment. Checks repeat during both soak phases. Deployment does not clear project quarantine markers. Automatic recovery resumes after the deployment fence clears in the active generation; operator-only failures still require the recovery procedure. Online preparation cancels when the deployment fence appears, keeping its serving runtime live while the worker stops.

Deploy version-two recovery readers with LOOMUP_SCHEMA_ATOMIC_ENABLED unset first. Release the compatible CLI and qualify a disposable project before enabling it with LOOMUP_SCHEMA_ATOMIC_ENABLED=1. Management status advertises schema_recovery_versions; candidate checks reject any release that cannot read a pending versioned journal. Rehearsal stages the manifest’s referenced query, command, and policy SQL files under their project-relative paths; missing files or SQL paths resolving outside the project fail before cutover. Old binaries reject version-two journals instead of interpreting their online recovery point as a rollback snapshot. The operation publishes draining before its durable cutover fence blocks requests. Policy-only changes inspect table definitions without rescanning existing user rows or repeating a database-wide foreign-key check during cutover. Structural changes retain existing-data validation. Atomic snapshot preparation uses SHA-256 and bounded sampled reads instead of a full integrity scan. Its five-minute stall timeout resets on worker progress, with a two-hour absolute ceiling. Scheduled recovery snapshots retain full integrity checks outside the deployment gate. Atomic cutover adopts an already-current event journal through metadata validation, as warm hosted runtimes do. Older journal schemas retain their upgrade/backfill path.

The qualified backend archive includes loomup-activate-release, the complete activation and rollback script. The deployment runner verifies the archive checksum before extracting this script and sending it over SSH. Keep activation logic in that packaged script so inline workflow expressions stay below GitHub's size limit and deployment uses the qualified revision.

The backend package includes loomup-schema-deployment-guard. It closes schema admission before waiting for active supervisors, holds the shared filesystem coordination lock exclusively through old-slot retirement, and leaves platform/projects/.locks/deployment-fence on coordinator failure. The workflow only clears it after verified completion or rollback to one healthy generation. Do not remove that file while either generation or a schema worker is still performing a deployment/migration. Reconcile failed rollouts and confirm one healthy serving generation plus no active schema supervisors before removing a stale fence and rerunning deployment.

For the first upgrade from a binary without schema_worker_isolation: true in its internal management status, deployment automatically installs the qualified Caddyfile.schema-fence through loomup-install-schema-proxy-fence. The helper adds an import to the existing tryloomup.com site, validates the candidate configuration, and reloads Caddy. Validation failure leaves the serving config unchanged; reload failure restores the prior configuration. An existing fence with different contents is rejected for operator review.

The temporary fence returns 409 with Retry-After for schema migration, Studio publication, and email schema writes. Deployment probes all four entrypoints before continuing, then waits for existing operations to finish using the new binary's read-only __schema-active command. It preserves the proxy fence through the blue/green overlap and removes the import only after the old generation stops. A rollback to the legacy binary keeps the temporary proxy fence for operator review. Later upgrades use filesystem coordination without a proxy change. Activation errors report their phase and script line without logging commands or secret values.

Before release, run the schema-worker fault regression and normal production qualification. During staging verification, force a migration timeout on a throwaway project while checking /, /ready, /__loomup/slot-health, the SQL dashboard, and a second project's HTTP and realtime traffic. Confirm the server PID/generation and traffic eligibility remain unchanged; inspect the target's terminal operation, prior revision, data, and recovery journal. After deployment, verify the exact released revision and the same public health endpoints.

Deployment probes check HTTPS through local Caddy (with TLS hostname verification) during host-side soak and schema-fence checks. The GitHub runner separately requires ten consecutive successful public readiness, account, and platform checks after activation. The three endpoints are checked concurrently within each sample; all must succeed, and any failure resets the consecutive sample count. A droplet-to-edge connectivity failure therefore does not masquerade as an unhealthy candidate; public availability remains a required deployment check.

Routine deployment observation windows use 10 warm, 20 promoted off-traffic, 30 live-traffic, and 10 post-demotion healthy samples. Full-qualification deployments retain 30/60/60/30 samples, as do direct activation invocations unless LOOMUP_DEPLOYMENT_SOAK_PROFILE=routine is explicitly supplied.

Production builds use two Cargo jobs and publish Cargo's HTML timing report plus GNU time's wall-time and peak-memory measurements in the qualification evidence release asset. Release and full-test caches are separate and keyed by the Rust toolchain, architecture, dependency manifests, vendored SQLite, Cargo settings, and workflow configuration. Routine source changes reuse dependency artifacts without saving another cache; generated evidence, databases, and the application crate's compiled artifacts are excluded. The exact application binary is always built, smoke-tested, checksummed, and packaged for the qualified revision.