Provisions six AWS or Vultr machines in one VPC and converges Langfuse v4.27.0 across
them: a Neon storage tier (storage broker, pageserver, one safekeeper,
Postgres 17 under compute_ctl, layers and WAL in R2), Redis 7.2, three
ClickHouse replicas each with a Keeper voter, and the app host running
langfuse-web, langfuse-worker and Caddy behind Cloudflare.
The Neon tier is not reimplemented here: this package SHA-pins
getcolors/neon and renders its templates
out of the installed package. The ClickHouse templates are this package's own, derived
from getcolors/clickhouse.
Install the launcher
npx skills add getcolors/langfuse
cp .agents/skills/package-langfuse-red/red ./red
chmod +x red
The root red is a copy of the payload, not a symlink. npx skills update -p rewrites the payload and leaves the copy alone, so copy it again
after every update or the project keeps running the old pin.
Verbs
./red build # render .colors/<profile>/ — no provider calls, no credentials
./red create --dry-run # walk the workflow, skip every side effect
./red create # converge for real; the gates run inside it
./red rehearse # restore both stores from backup, boot the pinned image, drill
./red describe # every host's last monitor result, over SSH
./red delete # guarded by compute-prevent-destroy; removes nothing in R2
build and --dry-run work on a fresh checkout with an empty environment.
Exit code 2 means validation failure and lists every problem at once.
Credentials
Non-secret desired state lives in colors.yml. Every credential is a
COLORS_PAR_* environment variable, conventionally in a gitignored
.envrc.private:
| Variable | For |
|---|---|
COLORS_PAR_VULTR_API_KEY |
VPC, firewall groups, instances, SSH key resource |
COLORS_PAR_CLOUDFLARE_API_TOKEN |
the DNS record; Zone:Read + DNS:Edit |
COLORS_PAR_R2_ACCESS_KEY_ID / _SECRET_ACCESS_KEY |
OpenTofu state only; reaches no host |
COLORS_PAR_LANGFUSE_STORAGE_R2_* |
events, media, exports (Object Read & Write on the storage bucket) |
COLORS_PAR_NEON_R2_* |
Neon layers and WAL — the deployment's .envrc maps the storage pair onto these |
COLORS_PAR_LANGFUSE_BACKUP_R2_* |
backups (Object Read & Write on the backup bucket) |
COLORS_PAR_LANGFUSE_ENCRYPTION_KEY |
64 hex chars, operator-held |
COLORS_PAR_LANGFUSE_SALT |
≥ 32 chars, operator-held |
COLORS_PAR_LANGFUSE_INIT_USER_PASSWORD |
≥ 12 chars, operator-held |
Never export COLORS_PAR_PROFILE: it selects the deployment's remote state.
The package refuses a create when the storage or backup pair equals the state
pair, or the backup pair equals the storage pair, unless colors.yml records
r2-credential-sharing: shared-accepted as a deliberate, committed line.
Three secrets outlive the app host
ENCRYPTION_KEY encrypts every stored secret and SALT hashes every API key.
A Postgres backup restored onto a fresh app host is readable only with the
same two values, so they are the operator's to hold, never generated on a disk
a rebuild would wipe. The init password logs you in after that rebuild, when
the generated project keys are gone. Keep all three outside these machines.
Everything else — Neon, Redis and ClickHouse passwords, NEXTAUTH_SECRET, the
project API keys — is generated on the host that owns it. sudo langfuse-credential on the app host prints the project keys.
What convergence guarantees
Gates that run on every converge and fail it if they fail:
- raw TCP from the app host to Neon, Redis and the ClickHouse client ports, and a refusal on Keeper — the network says what desired state says
UTCon both databases; three replicas visible throughclusterAllReplicas; an authenticated cross-node query; Keeper quorum;system.query_logpresent- a trace, a generation and a score ingested through the app-host API, read back, found on node 0 and the last replica, and a new raw-event object in R2
- a media file up through a presigned URL and back with the same sha256
- wrong API key, anonymous request, unauthenticated Redis
PING, wrong ClickHouse and Postgres passwords all refused - 200 traces queryable within the timeout, host memory under 85 %
- the backup credential cannot read live data; no backup credential in
ClickHouse's
query_log - the ready marker
<prefix>.colors-ready, written last and read back
./red rehearse then proves recovery: fresh sets, restore both stores,
boot the pinned image in a second Compose project on loopback with the
operator-held keys, read the trace, the project and an encrypted LLM
connection back through the API, stop a replica under ingestion, restart
Redis with a job queued, and write .colors-recovery-verified.
Recovery, stated honestly
| Failure | Recovers from | RPO |
|---|---|---|
| a ClickHouse replica | the other two, automatically | 0 |
| a Redis restart | the AOF | 0 |
| the Redis host | nothing automated: queued jobs are lost, their raw events stay in R2 | the queue |
| Neon local state | R2 layers + safekeeper replay | ~0 |
| the Neon host | the Postgres dump | 6 h |
| the whole ClickHouse cluster | the nightly native backup, paired with the next Postgres dump | 24 h |
| the app host | a converge with the operator-held keys; new project keys | 0 |
Backups pair at restore time: a ClickHouse set with the oldest Postgres dump completed after it, so Postgres is the newer snapshot and every project a restored trace references exists.
Managed AWS deployment
Use provider-compute: aws, provider-backend: s3, s3-bucket-mode: managed,
langfuse-storage-provider: s3, and langfuse-storage-managed: true.
The deployment creates the state bucket before Terraform initialization,
and creates separate Neon, Langfuse and backup buckets with scoped IAM
credentials. Use distinct globally unique bucket names and native S3
endpoints/regions in the existing *-r2-* configuration keys. The three
operator-held application secrets are still required.
Authorized delete removes the managed application buckets and their contents as well as the machines; the state bucket is finalized last. Existing R2 storage retains its prior preservation behavior.
The application smoke test uses loopback on the app host. Run an external client as well to prove public DNS, Cloudflare proxying and HTTPS.
Media in the browser
The API media path needs nothing more. UI rendering needs a CORS rule on the
storage bucket for https://<langfuse-host>, which only an Admin R2 token can
set; add it in the R2 dashboard or with wrangler r2 bucket cors put. The
smoke gate reports the preflight as WARN until it is there. Managed AWS
storage creates the CORS rule during bucket provisioning.
Configuration reference
See references/configuration.md.
Compute uses the pinned colors-compute library. The package declares roles
and security policy; the library owns shared and per-node OpenTofu state,
managed key lifecycle and cloud credentials. Use R2 or S3 state. Builds
render compute/shared and compute/nodes/<node_id>. Existing monolithic
<profile>/langfuse-infrastructure.tfstate requires explicit migration;
do not create over it or remove the refusal guard.