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):
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
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:
# 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:
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):
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 (mode0600on Unix; contains a project-local JWT secret)data/app.sqlite— portable SQLite database (WAL by default)- When object storage is enabled:
data/storage/(orstorage.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:
[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
| Setting | Behavior |
|---|---|
| (default) | All traffic uses the primary pool on database.path |
read_pool_size > 0, empty read_replicas | Same-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
| Config | Default | Notes |
|---|---|---|
database.max_size_bytes | 1 GiB | Payload size (page_count − freelist_count) × page_size. 0 = unlimited. |
storage.max_total_bytes | 100 MiB | Sum of object sizes across buckets. 0 = unlimited. |
storage.max_upload_bytes | 50 MiB | Per-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:
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
export LOOMUP_JWT_SECRET=...
loomup serve --config /var/lib/loomup/app/loomup.toml
Endpoints:
| Path | Purpose |
|---|---|
GET /health | Liveness (process up, DB open) |
GET /ready | Readiness: DB reachable and no pending application migrations |
http://HOST:PORT/admin | Admin UI |
http://HOST:PORT/api/{table} | REST CRUD |
ws://HOST:PORT/realtime | WebSocket 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:
[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:
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):
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):
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:
| Measurement | What it covers |
|---|---|
body | Bytes 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_queue | Time from submitting work through the server's blocking helper until a blocking thread begins it. |
blocking_work | Time executing that closure, including any nested DB/R2 work. A completed closure can still return an application error. |
db_pool | Connection checkout or schema-connection mutex wait in with_conn / with_conn_mut. |
db_work | Time inside those connection closures, including SQL, locks and other closure work; not SQL execution alone. |
r2_head, r2_get, r2_put, r2_delete | Object-store operation elapsed time, including library retries/backoff. Buffered GET includes reading the object body. |
r2_multipart | Multipart initialization, file reading, parts, completion and any abort cleanup. |
r2_get_headers | Streaming download GET until object response headers; excludes subsequent response-body streaming. |
project_router | Time inside a project router, recorded by its surrounding platform trace. |
authentication | Project JWT validation and current-user lookup, service-key lookup, password authentication, and platform session lookup. Includes repeated checks and any nested DB work. |
authorization | Resource/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_metering | HTTP 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. |
serialization | Project/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_transaction | Row-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_transaction | Row-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_sql | Transaction 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_commit | The 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_wait | Submission-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_json | Row 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_wait | Waiting 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:
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:
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:
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
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
loomup gen typescript --config loomup.toml --output ./src/loomup-types.ts
Process supervision
Use systemd, launchd, or your process manager. Example unit sketch:
[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:
- Terminate TLS at a reverse proxy (nginx, Caddy, Traefik).
- Set
server.behind_tls_proxy = truewhen binding a public interface, or - Set
server.allow_insecure_public_bind = trueonly 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.
Cookie mode
[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
[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:
$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):
| Setting | Meaning |
|---|---|
LOOMUP_CDN_BASE_URL | HTTPS origin, normally https://cdn.tryloomup.com; unset disables integration. |
LOOMUP_CDN_PROJECT_IDS | Comma-separated project allowlist; * enables delivery for all; empty issues no new redirects. |
LOOMUP_CDN_SERVICE_TOKEN | Shared 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.