One host
cloudd speaks plain HTTP. It does not terminate TLS. Put it on
loopback and let a front (Caddy or nginx) own the certificate, the public
port, and the HTTP/2 or HTTP/3 side; the front proxies to
127.0.0.1:8080. That is the whole recommended shape for one host:
internet ──443/tls──▶ Caddy or nginx ──http──▶ cloudd 127.0.0.1:8080 │ spawns one `hyphae serve` └─ per project on 127.0.0.1:20000+Tenant engines never leave loopback either. Nothing in this repository listens for TLS; if a certificate is in play, the front holds it.
Caddy obtains and renews the certificate itself (ACME). Replace the host name; keep the upstream on loopback.
cloud.example.com { encode zstd gzip reverse_proxy 127.0.0.1:8080 { # Function invokes may run up to CLOUDD_FN_TIMEOUT_MS; SSE streams # stay open until CLOUDD_REALTIME_IDLE_SECS. Do not buffer them. flush_interval -1 }}Caddy sets X-Forwarded-For and X-Forwarded-Proto on the way in. cloudd
ignores both unless told to trust that front (see CLOUDD_TRUST_PROXY
below), because anyone who can reach the daemon directly could otherwise
forge them.
The Studio, when CLOUDD_STUDIO_DIR points at a built studio/dist, is
served by cloudd itself at /studio/ on the same origin (/ redirects
there), so the front needs no extra route and no CORS: https://cloud.example.com/studio/.
The Studio bundle is tied to one deployment by three optional build-time
variables (make studio passes the environment through; the bundle must
never read them at runtime):
| Variable | Default | Effect |
|---|---|---|
VITE_SIGNUP_MODE |
open |
invite shows the invite-code field on the login page up front. On any build the field appears as soon as a signup is refused with 403 authorization_denied, so an open bundle still works against an invite daemon. |
VITE_DOCS_BASE |
/docs |
Prefix of every “Learn more” and “Docs” link: a path on this origin or an absolute URL (then links open in a new tab). |
VITE_SITE_ORIGIN |
unset | https:// origin of the public site; when set, the login page links to <origin>/cloud/access/ for requesting an invite. |
The docs site has the same split: make docs builds for /docs/ on the
daemon’s origin; make docs-public builds for https://hyphae.dev/cloud/docs/
(links to the Studio become absolute, light theme by default). Both come from
one content tree; the post-build checker fails on any leftover /docs/ link.
The equivalent server block, with the certificate paths you manage (certbot or otherwise):
server { listen 443 ssl http2; server_name cloud.example.com; ssl_certificate /etc/letsencrypt/live/cloud.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/cloud.example.com/privkey.pem;
client_max_body_size 40m; # ≥ CLOUDD_STORAGE_MAX_BYTES, ≥ CLOUDD_PROXY_BODY_LIMIT
location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; # SSE and long function invokes proxy_read_timeout 330s; # > CLOUDD_REALTIME_IDLE_SECS }}Starting the daemon
Section titled “Starting the daemon”Loopback is the default and needs no ceremony beyond the secrets decision:
CLOUDD_SECRETS_KEY=$(cat /etc/cloudd/secrets.key) \CLOUDD_DATA_ROOT=/var/lib/cloudd \CLOUDD_STUDIO_DIR=/opt/hyphae-cloud/studio/dist \clouddcloudd refuses to start, naming the variable, when:
CLOUDD_BINDis a non-loopback address (0.0.0.0,::, a LAN address) and eitherCLOUDD_PUBLIC=1is missing orCLOUDD_SECRETS_KEYis empty. Both are required together. Loopback never needsCLOUDD_PUBLIC. Binding publicly is for the case where the TLS front runs on another machine or in another network namespace (a container); it is still not TLS, and it still expects a front.CLOUDD_TRUST_PROXY=1is set andCLOUDD_PROXY_NETSis empty. Trusting forwarded headers from everyone is the same as trusting no one.- neither
CLOUDD_SECRETS_KEYnorCLOUDD_ALLOW_EPHEMERAL_SECRETS=1is set (function secrets at rest need an explicit decision). CLOUDD_SIGNUP_MODE=invitewithoutCLOUDD_INVITE_CODES_FILE, or with a file that is missing, empty, or readable by others (mode bits0o077). An open signup on a public host is a decision, not a default that happened by omission.
Generate a secrets key once and keep it outside the data root:
openssl rand -hex 32. Rotating it orphans existing function secrets.
Run it under a supervisor (systemd Type=simple, KillSignal=SIGTERM,
TimeoutStopSec=10). On SIGTERM the daemon stops accepting, ends every
SSE stream, kills every in-flight function worker’s process group, puts the
tenant engines to sleep, and exits within a 5 s bound even if a listener
or a sleeping function is still attached (tested in
crates/cloudd/tests/process.rs).
Health
Section titled “Health”Both routes are unauthenticated, JSON, and never carry a project name, a key, or a secret. Point the supervisor at the first and the load balancer at the second.
| Route | Answer | Meaning |
|---|---|---|
GET /healthz |
200 {"ok":true} |
The process is up and answering HTTP. |
GET /readyz |
200 {"ok":true,"store":true,"engine":"ok (3.0.0)","projects":N} |
The control store reads and, if any project exists, the pinned hyphae binary is found. engine is unchecked while there are no projects. |
GET /readyz |
503 {"ok":false,…,"engine":"unavailable"} |
Store unreadable or engine missing/wrong version. The reason is in the daemon log, not in the body. |
Environment
Section titled “Environment”The full table is in the README; these are the ones that matter in front of a terminator.
| Variable | Default | Meaning |
|---|---|---|
CLOUDD_BIND |
127.0.0.1:8080 |
Listener. Loopback needs nothing else. |
CLOUDD_STUDIO_DIR / CLOUDD_DOCS_DIR |
unset | Built Studio (/studio/) and docs site (/docs/) to serve from the same origin. install.sh puts them under /usr/local/share/hyphae-cloud/. |
CLOUDD_SIGNUP_MODE |
open |
invite makes POST /v0/signup require a code from CLOUDD_INVITE_CODES_FILE (one per line, 8–128 bytes, optional label; 0600, owned by the daemon user). Missing/unknown code → 403 authorization_denied, same body either way, before any account lookup. The daemon refuses to start in invite mode without a readable, private, non-empty file. Codes are hashed on load, compared over the whole list, never logged or receipted (a receipt records the entry’s index and label). Rotation: edit the file, systemctl restart hyphae-cloud. |
CLOUDD_PUBLIC |
unset | 1 permits a non-loopback CLOUDD_BIND, and only together with a non-empty CLOUDD_SECRETS_KEY. Does not add TLS. |
CLOUDD_SECRETS_KEY |
unset | 64 hex chars (or exactly 32 raw bytes) for function secrets at rest. Mandatory for a public bind; otherwise mandatory unless the next variable says so. |
CLOUDD_ALLOW_EPHEMERAL_SECRETS |
unset | 1 derives a development key from a salt file under the data root. Loopback and development only; ignored as a substitute for CLOUDD_SECRETS_KEY when binding publicly. |
CLOUDD_TRUST_PROXY |
unset | 1 honours X-Forwarded-For / X-Forwarded-Proto, but only on connections whose peer address is inside CLOUDD_PROXY_NETS. Off, the headers are ignored and the TCP peer is the client. |
CLOUDD_PROXY_NETS |
empty | Comma-separated CIDRs of the front(s), e.g. 127.0.0.1/32 or 10.0.0.0/24,fd00::/64. Required when CLOUDD_TRUST_PROXY=1. |
CLOUDD_FN_TIMEOUT_MS |
5000 |
Wall-clock cap per function invocation; the worker’s process group is killed and the caller gets 504. Set the front’s read timeout above it. |
CLOUDD_STORAGE_MAX_BYTES |
33554432 |
Cap on one stored object (32 MiB). Set the front’s body limit at or above it, and above CLOUDD_PROXY_BODY_LIMIT (8 MiB) for /v2. |
The client address and scheme resolved under these rules appear in the
access log (cloudd::access) as client= and scheme=, with
forwarded=true when a trusted front supplied them. No route changes its
answer based on them in this phase; they exist so the log is truthful.
Backing up the control plane
Section titled “Backing up the control plane”The Cloud’s own authority — as opposed to tenant data, which is Native’s —
is three things under CLOUDD_DATA_ROOT:
| File | What it is |
|---|---|
control.json |
Accounts (password hashes), sessions, projects, keys (verifiers), snapshot catalog, blob catalog, function metadata, encrypted function secrets, receipts. |
secrets.salt |
Only with CLOUDD_ALLOW_EPHEMERAL_SECRETS=1: the salt the dev secrets key is derived from. Without it, secrets in the store cannot be opened. |
functions/<project>/<name>/index.js |
Function sources (CLOUDD_FUNCTIONS_ROOT). The Cloud-owned runners in functions/_runtime/ are regenerated at start and not backed up. |
cloudd control-backup copies exactly those into a new directory and writes
a manifest; cloudd control-restore verifies the manifest and puts them
back. Both are subcommands of the daemon binary and read the same
environment (CLOUDD_DATA_ROOT, CLOUDD_FUNCTIONS_ROOT). There is
deliberately no HTTP route for either: restoring the authority file under a
running daemon is not a thing this daemon does, and an operator token would
be one more secret to rotate.
# Any time; the daemon may be running (the store is written by rename, so# the copy is a whole file). Prints the manifest.CLOUDD_DATA_ROOT=/var/lib/cloudd cloudd control-backup --out /var/backups/cloudd# → /var/backups/cloudd/20260915T120301Z-8f3a1c2e/{MANIFEST.json,control.json,secrets.salt,functions/…}MANIFEST.json carries id, created_at, cloudd_version,
project_count, and sha256 per copied file. That is Cloud metadata, a
checksum over the copied bytes; it is not a Native proof and says nothing
about tenant data.
When to back up: before upgrading cloudd, before rotating
CLOUDD_SECRETS_KEY, after creating projects or keys you cannot afford to
re-issue, and on a schedule (a cron line calling the command above is the
whole scheduler). Keep the backup directory off the host; the files are
0600, and control.json contains password hashes and key verifiers —
treat it as sensitive even though no plaintext secret or key is in it.
What is not included:
- Tenant volumes (
projects/<id>/native). Those are Native’s; usePOST /v0/projects/{id}/snapshots(hyphae backup create) per project. A control backup restored without its tenant directories yields projects whose engines have no data. - Blob bytes (
CLOUDD_BLOB_ROOT, or the S3 bucket). The catalog is in the store; the payloads are not. Copy the blob root (or the bucket) separately, ideally right after the control backup so catalog and bytes agree. cloudd.lock, logs, replaced data directories, and anything undersnapshots/.
Restore is destructive (to the control files; never to tenants):
systemctl stop cloudd # tenants are put to sleep on the way downCLOUDD_DATA_ROOT=/var/lib/cloudd cloudd control-restore --from /var/backups/cloudd/20260915T120301Z-8f3a1c2esystemctl start clouddRestore refuses, touching nothing, when a daemon holds the data root’s
cloudd.lock (so no tenant can be awake), when any file’s SHA-256 differs
from the manifest, or when the manifest is missing. On success the current
control.json, secrets.salt, and functions tree are moved aside with
a .pre-restore-<stamp> suffix, not deleted; delete them yourself once the
restored daemon looks right. Every project comes back asleep. Sessions in
the backup are restored too, so a token issued after the backup stops
working and one issued before it works again until it expires.
Keys and salts. Function secrets are encrypted under
CLOUDD_SECRETS_KEY (or the salt-derived dev key). A backup taken under
one key is only useful with that key: restoring onto a host with a different
CLOUDD_SECRETS_KEY brings the secret names back but every invoke that
needs one fails with a clear error until the owner sets the value again.
Rotate the key by taking a backup, changing the variable, and re-setting
each secret. There is no operator token to rotate: the only credentials in
play are the host’s file permissions on the data root.
Full host backup
Section titled “Full host backup”scripts/backup-all.sh ties the pieces above into one operator kit for a
stopped data root: the control backup, one Native backup per tenant
that has a data directory, a copy of the filesystem blob root, and a
KIT.json saying what was and was not captured. It is a bundle of things
taken one after another — not a Native snapshot of the host and not
point-in-time across tenants. Each tenant’s NATIVE_BACKUP.json is
Native’s statement about that tenant; KIT.json is Cloud metadata.
systemctl stop cloudd # SIGTERM: tenants are put to sleep by the daemon, never by the scriptscripts/backup-all.sh \ --data-root /var/lib/cloudd \ --out /var/backups/cloudd/kit-$(date -u +%Y%m%dT%H%M%SZ) \ --cloudd-bin /usr/local/bin/cloudd \ --hyphae-bin ~/.cache/hyphae/3.0.0/bin/hyphae \ --blob-root /var/lib/cloudd/blobssystemctl start clouddWhat it does, in order:
- Takes the same
flockon<data_root>/cloudd.lockthat the daemon holds. If a daemon is running it exits 1 and says to send SIGTERM; it never signals anything itself. cloudd control-backup --out OUT/control(readsCLOUDD_DATA_ROOT, set by the script from--data-root;CLOUDD_FUNCTIONS_ROOTis honoured if you export it).- For every project id in the store (
cloudd control-projects): if<data_root>/projects/<id>/nativeexists,hyphae backup create --data-dir <that> --out OUT/tenants/<id>with the pinned 3.0.0 binary (a binary reporting any otherengine_versionmakes the script refuse). No engine → every such tenant is listed undertenants_skippedwithskipped_engineand the kit is still written. Projects never woken have no directory and are listed withno_data_dir. cp -aof--blob-rootintoOUT/blobs/— filesystem backend only. WithCLOUDD_BLOB_BACKEND=s3in the environment the kit records"blobs": "external"and copies nothing: the bucket is backed up with the bucket’s own tooling, out of band. No--blob-root→"absent"(with a note on stderr if<data_root>/blobsexists).OUT/KIT.json:created_at,control_backup_id,engine,projects,tenants_backed_up,tenants_skipped,blobs(copied|external|absent), andsha256of the kit-level manifests (the controlMANIFEST.jsonand each tenant report).
OUT must be a fresh, empty directory per kit. Move the kit off the host;
it contains everything the control backup contains (see above) plus tenant
data.
Restore order — written, not automated. There is no kit restore
script; a half-automated restore would be worse than none. Do it in this
order, with cloudd stopped throughout:
- Control first.
CLOUDD_DATA_ROOT=/var/lib/cloudd cloudd control-restore --from OUT/control/<control_backup_id>— refuses if a daemon holds the lock or a digest differs; moves the current control files aside. - Each tenant. For every id in
tenants_backed_up:hyphae restore --backup OUT/tenants/<id> --data-dir /var/lib/cloudd/projects/<id>/native.restoredthen move the currentnativeaside and renamenative.restoredtonative. (This is exactly whatPOST /v0/projects/{id}/restoredoes for a Cloud snapshot; the kit’s tenant backups are the same Native format, so they can also be restored through that route once the daemon is up, one project at a time.) Tenants intenants_skippedkeep whatever directory is on disk. - Blobs last.
cp -a OUT/blobs/. /var/lib/cloudd/blobs/if"blobs": "copied"; forexternal, restore the bucket with the bucket’s tooling. The catalog came back in step 1, so any object whose bytes are missing will 404 on download until this step is done. - Start
cloudd. Every project comes back asleep; wake them from the Studio,hcloud, or the API.
AWS single-host
Section titled “AWS single-host”deploy/aws/ is the first cloud deploy and it changes nothing about the
process model: one EC2 instance, cloudd on 127.0.0.1:8080, one hyphae serve per project on loopback, the data root on one EBS volume. TLS is
terminated at CloudFront (tls_at = cloudfront, the default): the
distribution owns the certificate (ACM, us-east-1), speaks HTTP/2+3 to
viewers, caches nothing, forwards every viewer header, and reaches Caddy on
the instance’s :80 over plain HTTP — the security group admits only
CloudFront’s origin-facing ranges there. tls_at = caddy is the
no-CloudFront shape where Caddy holds the certificate and 80/443 are open. Terraform (validated with OpenTofu, never
applied from this repository), a cloud-init user-data.sh, a systemd unit,
a Caddyfile, and a release tarball from make dist. Full detail and every
flag: deploy/aws/README.md. The click-path:
- Build the tarball on Debian 12’s glibc:
make dist-bookworm(ormake diston such a box), thenmake dist-check. - Provision:
cd deploy/aws/terraform && cp terraform.tfvars.example terraform.tfvars(region, name,domain),terraform init && terraform apply. Creates the security group (80/443), the blobs and artifacts buckets, a role that may read/write only that blobs bucket, read only the tarball, and read only one SSM parameter, an encrypted gp3 volume withprevent_destroy, the Debian 12 instance (IMDSv2), and an Elastic IP. The volume is attached as/dev/sdfand mounted by user data at/var/lib/hyphae-cloud. - Secrets key:
aws ssm put-parameter --name "$(terraform output -raw ssm_parameter)" --type SecureString --value "$(openssl rand -hex 32)". User data reads it into/etc/hyphae-cloud/secrets.env(0600); it is never in the AMI, the tarball, or Terraform state. Without itclouddrefuses to start, by design. - Tarball:
aws s3 cp dist/hyphae-cloud-0.1.0-linux-x86_64.tar.gz s3://$(terraform output -raw artifacts_bucket)/. User data downloads it, checksSHA256SUMS, runsinstall.sh. (Or scp it and runsudo ./install.shyourself.) - DNS: an
Arecord fordomain→terraform output -raw public_ip. Caddy obtains the certificate once the name resolves. - Check:
curl https://$DOMAIN/healthz→{"ok":true};curl https://$DOMAIN/readyz;https://$DOMAIN/studio/in a browser.
With domain empty, Caddy serves plain HTTP on :80 so you can curl
/healthz — that is a smoke test, not a real deploy: no TLS, and a
Studio session token would cross the wire in clear. Set the domain before
creating any account you care about.
On this host blobs go to S3 over HTTPS: CLOUDD_BLOB_BACKEND=s3,
CLOUDD_S3_ENDPOINT=https://s3.<region>.amazonaws.com,
CLOUDD_S3_BUCKET=<blobs bucket> (all written by user data from Terraform
outputs), signed with SigV4. Credentials: cloudd tries
CLOUDD_S3_ACCESS_KEY/SECRET_KEY, then AWS_* environment, then
~/.aws/credentials, then IMDSv2 (session token signed), and refuses to
start with none. Here the first one is used — a dedicated IAM user’s key
stored in SSM and written to secrets.env at boot — because user data
blocks the instance metadata service for the cloudd uid so that project
functions, which run as that uid with this host’s network, cannot obtain
the instance role’s credentials. That deny stays; the static key is the
consequence, not an oversight. Rotation and teardown: deploy/aws/README.md.
First AWS smoke (2026-09-15)
Section titled “First AWS smoke (2026-09-15)”The single-host stack is deployed and reachable at https://infra-hyphae.run
through CloudFront. What exists, all created by deploy/aws/terraform
(tls_at = cloudfront, region us-east-1, name hyphae-cloud):
| Thing | Id / name |
|---|---|
| Public URL | https://infra-hyphae.run (www. and plain http:// 301 to it) |
| CloudFront distribution | E3CUZB5Q3YAHV6 (dic7uue0olw38.cloudfront.net), TLS 1.2+, HTTP/2+3, CachingDisabled, AllViewer, all methods |
| WAF web ACL (CLOUDFRONT scope, us-east-1) | hyphae-cloud-edge — rate rule 500 req / 5 min / IP → block; AWSManagedRulesCommonRuleSet in count (its body-size and injection heuristics would reject legitimate function sources and object uploads; read its counts before ever switching to block) |
| ACM certificate (us-east-1) | …:certificate/11f52c51-47d4-4d95-8592-3857311263fe, infra-hyphae.run + www, ISSUED via Route 53 validation |
| Route 53 | zone Z04465543PQWN7YWAXS3X: A/AAAA aliases for apex and www → CloudFront, two ACM validation CNAMEs |
| EC2 instance | i-0a33fa6493251c190 (Debian 12, t3.small, IMDSv2), Elastic IP 3.223.25.205 (eipalloc-0a2d9d41da440136f) |
| Security group | sg-0cd43f1950900e655: ingress only tcp/80 from prefix list pl-3b927c52 (com.amazonaws.global.cloudfront.origin-facing); no 22, no 443, no 0.0.0.0/0 |
| EBS data volume | vol-0871cdb05e3e36872, 50 GiB gp3, encrypted, prevent_destroy, mounted at /var/lib/hyphae-cloud |
| S3 | blobs hyphae-cloud-blobs-456859598621 (SSE-S3, public access blocked); artifacts hyphae-cloud-artifacts-456859598621 (tarball + SHA256SUMS) |
| IAM | role/profile hyphae-cloud-instance (tarball read, SSM prefix read, SSM agent core, blobs policy); user hyphae-cloud-blobs (blobs policy only; one access key, stored in SSM) |
| SSM parameters | /hyphae-cloud/hyphae-cloud/{secrets_key,s3_access_key,s3_secret_key} (SecureString) |
Smoke results, all through CloudFront: /healthz 200, /readyz 200
(engine: ok (3.0.0)), /studio/ 200; hcloud signup → project
prj_221db12762ce352b → wakeup (Native 3.0.0 running on the instance) →
key → /v2/capabilities decoded → head live probe → sleep → snapshot
snap_71a9c80623b286c4 (hyphae backup create on the instance); storage
put of a 31-byte object → aws s3api head-object on
hyphae-cloud-blobs-…/prj_221db12762ce352b/assets/docs/hello.txt (SSE
AES256) → get round-trips identical; function deploy + invoke → 200;
Studio browser smoke 16/16 (including SSE through CloudFront). On the
instance (via SSM Run Command, no SSH): S3 credentials resolved source="static (CLOUDD_S3_ACCESS_KEY)", blob backend ready backend="s3",
functions engine ready runtime="node", the nftables IMDS deny present and
effective (curl as cloudd to 169.254.169.254 refused), secrets.env
0600, no secret string in the user-data log. From a non-CloudFront address,
http://3.223.25.205/healthz times out (security group).
Two things went wrong and were fixed during the deploy, both now in the repo: the Debian 12 AMI ships no SSM agent (user data installs the official package) and its Node is 18 (user data installs Node 22 LTS from nodejs.org, checksum-verified; cloudd now refuses a node older than 20 at detection time instead of failing every invoke). The instance was replaced once to pick those up.
Operating it. Shell: aws ssm start-session --target i-0a33fa6493251c190.
Logs: journalctl -u hyphae-cloud -u caddy. Upgrade: upload the new
tarball, then on the box systemctl stop hyphae-cloud, unpack,
./install.sh, systemctl start hyphae-cloud. Backups: the kit
(hyphae-cloud-backup-all) with the daemon stopped; blob bytes are in the
bucket ("blobs": "external"). The Terraform state is local to
deploy/aws/terraform/ on the operator machine and holds no secrets; keep
it. The AWS CLI on that machine uses aws login session credentials, which
the provider cannot read — run tofu as
eval "$(aws configure export-credentials --format env)" && tofu ….
2026-09-15, signup closed. The daemon runs with CLOUDD_SIGNUP_MODE=invite
(codes from SSM …/invite_codes, installed by user data as
/etc/hyphae-cloud/invite-codes, cloudd:cloudd 0600). The Studio bundle on
the box is built with VITE_SIGNUP_MODE=invite, so the login page asks for
the code up front; hcloud signup --invite-code and the JS SDK’s
auth.signup({ invite_code }) are the other two doors. Upgrades go through
SSM Run Command (AWS-RunShellScript, sh — no pipefail) with the exact
stop → install.sh → start sequence above; the browser smoke runs against
production with SMOKE_INVITE_CODE read from SSM into the environment.
Teardown (not run; AWS_DESTROY=1 is the gate):
cd deploy/aws/terraformeval "$(aws configure export-credentials --format env)"aws iam list-access-keys --user-name hyphae-cloud-blobs --query 'AccessKeyMetadata[].AccessKeyId' --output text \ | xargs -n1 aws iam delete-access-key --user-name hyphae-cloud-blobs --access-key-idaws s3 rm s3://hyphae-cloud-blobs-456859598621 --recursiveaws s3 rm s3://hyphae-cloud-artifacts-456859598621 --recursive# The data volume is the only copy of every tenant: remove `prevent_destroy` in main.tf on purpose, thentofu destroyaws ssm delete-parameters --region us-east-1 --names /hyphae-cloud/hyphae-cloud/secrets_key /hyphae-cloud/hyphae-cloud/s3_access_key /hyphae-cloud/hyphae-cloud/s3_secret_keyWhat this still is not
Section titled “What this still is not”- Not TLS. cloudd has no certificate, no ACME client, no
https://listener. Remove the front and you have plain HTTP. - Not multi-host realtime. The SSE fanout is in-process; two daemons do not share subscribers or events. One host, one daemon — on AWS too: one instance, one volume, no multi-AZ, no failover.
- Not an HA control store. The control store is one file under
CLOUDD_DATA_ROOTwritten by one process;control-backupand the kit copy it, nothing replicates it. There is no failover, for the control plane or for tenants (CLAUDE.md, invariant 6). - Compose is unverified as Compose.
deploy/compose/docker-compose.ymldescribes the same image and environment, now withCLOUDD_PUBLIC=1(the bind is0.0.0.0inside the container namespace, published on host loopback) and a mandatoryCLOUDD_SECRETS_KEY; it has never been executed withdocker composeon the development machine, and the image was not rebuilt for this phase. Treat it as documentation until a run is recorded in the README. - No OAuth, no KMS, no sandbox. Function secrets are encrypted with the
process key above, functions are trusted project code
(
docs/product/claims.md), and accounts are password sessions.