Repository navigation
Keel in Go: one binary for control plane, edge, agent and CLI - #30
Conversation
Copied from apps/cli unchanged; the old module stays until its commands are ported onto the HTTP API. CONFLICT, UNAVAILABLE and FORBIDDEN join the code vocabulary (codes are only ever added). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…embedded dashboard
- serve constructs the jobs scheduler and the realtime server, sets
app.Jobs/app.Events, mounts GET /api/ws, runs app.Recover once the
listener is up, and shuts down in order: HTTP, recovery pass, jobs,
WebSockets, adapters, database.
- adapters.go: one TODO hook per external adapter (swarm, proxy, axiom)
for the integration step.
- /config.js: window.__KEEL__ = {"apiUrl","version"} (cli-install.md B3).
- apps/web/embed.go: dist embedded with -tags embedweb, nil otherwise;
main sets cli.WebFS.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… + Better Auth scrypt) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Domain: node defaults, engines, seeded credentials, naming, placement, reference syntax and resolution (depth 5, encodeURIComponent), referrer graph, reference rewriting. App: projects.ts, environments.ts, nodes.ts, variables.ts use cases; computeEnv and markReferrersDirty seams. SQLite: CanvasTx queries, unique node name per environment (0002_canvas.sql). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nd their SQLite store beginDeployment and the Ship use case, the deployment reads, step writers with the 500-line log cap, apply (per-node serialized, skips a superseded revision before createOrUpdate), observe (one node, sweep, servers), the debounce, reconcile of the affected environment, the 5-minute timeout and the deploy part of recovery. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Domain rules of proxy-ingress.md §3 (shortHash, default and moved domains, validDomain, port allocation, cert hints), the expose/unexpose use cases with their exact checks and messages, followPort, ScheduleProxySync (Jobs key proxy:sync, serialized passes that go again when endpoints moved), failure attribution, statuses written only when they change, cert reports and the start-up pass (default domains follow the public IP, sync, 2-minute resync). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Root Dockerfile: bun builds apps/web/dist (turbo prune, so it follows whatever workspace packages web depends on), Go embeds it with -tags embedweb and stamps cli.Version, alpine runtime with CA certs and tzdata, ENTRYPOINT keel, CMD serve, /data volume, port 8080. Makefile: dev (keel serve on 127.0.0.1:3400, state in ./.keel), test, openapi, web, build, image. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Projects, environment summary, nodes (list, create, update, position, duplicate, start, stop, delete) and variables (list, referenceable, set, delete) per the resolved API decisions; NodeView per projects.md 4.3. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Use cases against a temp SQLite with fake Jobs/Proxy/Publisher: loaded config and statuses, unchanged resync writes nothing, cert states, listener blame and retry, whole-load failures, a sync that goes again when endpoints move during a load, cert reports racing a sync, and the default-domain move at start. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
adapters/swarm: toSpec, image inspect/pull (in-stream errors fail the pull), service create/update/remove, observation reads, server count, and the keel-agent global service that replaces scripts/deploy-worker.sh. Routes shipEnvironment, getLatestDeployment, getDeployment (null envelopes) and listNodeDeployments; the agent's events route with the Convex bearer, body limit and parsing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
exposeNode / unexposeNode / getControlPlane (tag ingress) and the raw cert-report route with the Convex answers: worker bearer compared in constant time, 256 KiB counted in UTF-16 units, bad json / bad report. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e_modules The local Docker has no buildx: drop cache mounts and $BUILDPLATFORM (release images are built natively per arch). ignore node_modules keeps ./... patterns out of the JS workspace's dependencies. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ting Table tests for frames, lines, sinceAfter, dockerSince, eventsSince, replica, sinkKey and the Axiom URL (expected values from the worker's TypeScript under node). httptest for /worker/* and Axiom ingest (backoff schedules via a fake sleep); fake Docker and sinks for the forwarder and the shipper, including the organization isolation of log routing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Log sinks per organization, Sign in with Axiom (DCR + PKCE), logs tail (Docker and Axiom), environment logs, trace overview/get/around, the OTLP relay, the tracing switch, withTracing and the agent prompt. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
One image, ghcr.io/thallesp/keel, runs both control-plane containers: `keel serve` (API, dashboard, SQLite in keel-data, Swarm driver that manages keel-agent itself) and `keel proxy` (the edge, today's container settings and volumes, never the Docker socket). The Convex backend, web, functions image and worker deploy script are gone. install.sh keeps its contract and only adds: apiUrl, convexImportNeeded and warnings in the JSON (convexUrl/convexSiteUrl stay, equal to url), KEEL_ACME_CA passed through, KEEL_SITE_URL in the state file. New installs write no Convex secret; upgrades keep theirs, keep keel_convex-data, and warn on every run until the data is imported. CI: go (gofmt, vet, test on the root module), web (ui + web typecheck, web build), install end to end against the new auth and device-login routes, plus an upgrade over a Convex-era data volume. images.yml builds the one image; the old images' tags stay published. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
adapters/caddy implements app.Proxy: GET /keel/host-addrs, GET /keel/certs, POST /config/apps (a refused load is app.ProxyRejected with Caddy's "loading config:" prefixes stripped), a 30 s idle timeout and the "keel-proxy is not running (no admin socket at ...)" error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-only guard Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…gin (app, sqlite, HTTP) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Huma operations for the sink, Sign in with Axiom, logs, traces and the tracing switch; raw POST /otlp/v1/traces and GET /worker/config (alias /agent/config). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reference syntax (ECMAScript whitespace), resolution and depth limit, encodeURIComponent, referrer graph, naming and placement as table tests; projects, nodes and variables use cases against a temp database with fakes for joinOrFound, beginDeployment and the delete schedulers, including a foreign-organization access test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… timeout, recovery, events Table tests for every exact algorithm; use cases on a temp SQLite with a fake Swarm and a manual job scheduler on a fake clock: Ship rules and messages, organization isolation, apply serialization and the stale-revision skip, the debounce, the timeout and its re-arm after a restart. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ne client KEEL_TS_AUTHKEY set: the agent reaches KEEL_URL through an ephemeral tsnet node keel-agent-<hostname> (state in a temp dir, 2 min to join); unset: http.DefaultTransport over the host network, as today. tsnet adds about 20 MB (keel 20.7 -> 41.2 MB, stripped 14.0 -> 28.2 MB), under the 40 MB threshold, so it is compiled in without a build tag. go.mod: tailscale.com v1.104.1 (bumps otel to v1.44 / otelhttp v0.69 through MVS); moby client and api become direct requirements. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r API on httptest) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
NodeView derivation (deploy step, error, stoppedAt, finishedAt, publicUrl, endpoints) and its JSON; the canvas routes over httptest: envelopes, 201/204 statuses, problem codes and sentences, null reads for foreign members, idempotent deletes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ow, foreign organization) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
internal/proxy embeds Caddy v2.11.7 and caddy-l4 v0.1.2 as libraries with only the modules proxy-ingress.md §12.2 lists, plus Keel's plugin ported from apps/proxy: hostns.go verbatim (host-tcp/host-udp), the admin routes /keel/host-addrs and /keel/certs, the `keel` cert event handler. Base config from apps/proxy/caddy.json, admin socket at KEEL_PROXY_SOCKET, --resume from Caddy's autosave, ACME terms agreed as Caddy's command does. Registered as `keel proxy` through cli.Extra. Tests: hostns table, reporter filtering/retries, admin routes, the §6.3 config provisioning with exactly these modules, and the edge end to end on loopback (admin socket, layer4 proxying, refused load keeps the old config, resume after restart). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…eployment routes and envelopes) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ge keeps 30 s Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Caddy takes the binary from 18 MB to 76 MB; a CLI-only build can drop it with -tags keel_noproxy (18.8 MB). Default builds keep `keel proxy`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ARCHITECTURE.md still described the first port: the recovery pass run inline by serve, api/ importing only stdlib, a SITE_URL fallback, the api.Code* vocabulary and ch.Node without its environment. It now says the recovery pass is a job with three parts, runtime state lives on App, write runs AfterCommit funcs, the worker protocol (array-only events, one config entry per organization sink, typed proxy reports), the shared caddy.DefaultSocket, RATE_LIMITED, and the CI checks that gate a change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
GET /worker/config is one entry per organization sink with no project id, POST /worker/events takes only JSON arrays ([] as the resync hello, one element per event), the agent and observe log through slog and only on failure, and the agent finds Docker through DOCKER_HOST. The two docs still described per-project routes, NDJSON, config applied lines and DOCKER_SOCKET; they also gain the Axiom row decoding's window and column rules and the embedded tracing prompt. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
root.go registers serve, openapi and agent itself; only the build-tagged proxy command goes through extra, and main hands the dashboard FS to Execute. The client returns api types unless the printout differs. The first account founds the organization at sign-up, not at project create. Unexpose checks its selector with Expose's rules before looking for the endpoint. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The specs stay as history, but where a section prescribes behaviour the Go code deliberately dropped (JavaScript string and number semantics, Better Auth's scrypt, stored device code client and interval, the keel-worker volume and DOCKER_SOCKET, Sprintf log templates, NDJSON and single-object worker events, projectId in the worker config, /config.js, migrations.run and the Convex import), a one-line "Go now" note says what the code does instead. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
GET /api/auth/device is public and unlimited, so a signed-out lookup must not queue on the single writer connection, as fcbdca4 intended; 735c9d0 folded it into the claim's write tx. Only a signed-in claim writes now, and a test holds the writer to pin it. ClaimDeviceCode returns just the status: the user code it echoed back is the caller's own input, which the handler already has. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The server has sent {"orgs":[]} since arrays stopped being null on the
wire (4be26ba), but the spec still said null and the Axiom gate tested
`if (orgs)`. An empty array is truthy, so a fresh install showed an
empty org picker and never the Sign in with Axiom button. The spec now
says what the server sends, and the gate checks for a non-empty list.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fa5762d left the services-only rule to the server, whose INVALID_INPUT carries no fix and does not name the node. tracing status still checks locally, so the verbs disagreed. The guard is back, with a test that also proves the server is never asked. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Deployment.Status, DeployStep.Status, NodeView.Type and NodeView.Status were plain strings, so DeploymentOf and NodeViewOf converted with string(...) and the CLI compared against "running", "success", "failed", "group", "service", "off" and "old". With the domain types the CLI uses the constants; openapi.json is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
applySuperseded was a one-line method over writeStep that re-passed the deployment and node apply already captures in step. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
canvasTouch read every node of the environment to add one /api/nodes/<id> topic each, but ch.Environment already publishes the /api/nodes/ prefix, which the dashboard matches by prefix. The read, the loop and an error branch in eight callers go. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
deployComputeEnv and deployFollowPort were package-level variables only so tests could swap them. The deploy tests now set real variables and a real endpoint, so they check what apply actually sends to Swarm and that a port change moves the endpoint. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
canvasShip and canvasSchedulers were package-level variables tests swapped. Shipping is now a field New sets to beginDeployment, which a test stubs per App. RemoveNode calls the schedulers directly, and the canvas tests read what they scheduled from a fake Jobs, as the proxy tests already do. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
app.truncateRunes was a copy of domain.TruncateRunes. app calls the domain one, and its test table moved next to it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
endpoints.domain, public_port and status_error were nullable, so the adapter went through str, nullStr, ptrInt, intOr0 and ptrInt64 and Endpoint.PublicPort was a *int whose nil Key and Address printed as "0". The columns are NOT NULL with '' and 0, the unique indexes skip those, PublicPort is an int, and the three int converters are gone. The wire is unchanged: publicPort is still left out when unset. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Nothing writes or reads it since Deployment.Sha went (06b0714). The schema note on node names also said the use cases check first; a rename relies on the index alone, so it now says what holds. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
orgSinkOf returned *domain.LogSink with nil meaning no sink, so its callers compared to nil and dereferenced. It returns (sink, ok, err) now; the OTLP relay needs only the zero value's empty Traces. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
NewControlPlane wrapped a struct literal while the Forwarder next to it was built inline, and Agent held the whole Config to read ConfigPoll. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
It used a *Position as the "no node yet" marker and dereferenced it at the end; a found flag and the zero Position say the same. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign-up founds the organization (32412f1), and ensureDefaultProject answers NO_ORGANIZATION to an account without one, so the founding branch that refreshed the session after it could not run. It goes, with useAuth().refreshSession, its only caller, and the comments that described the old flow. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
It still said the first keel project create founds it, which stopped being true when sign-up started founding it (32412f1). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
StartedAt, AppliedAt and FinishedAt on steps, FinishedAt on deployments and Node.ShippedAt were *int64 over nullable columns, so every write was new(now) and every check a nil compare. The columns are NOT NULL DEFAULT 0 and the fields int64. The wire is unchanged: the API fields keep omitempty, so an unset time is still left out. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
apply_error, observed_state, observed_error, the node_id of deployment steps and log lines, and log_sinks.traces and org were nullable, so the adapter wrapped every read in str and every write in nullStr while the domain fields were plain strings. They are NOT NULL DEFAULT '' now. str and nullStr stay for the foreign keys (parent_id, user_id), where NULL is what SQLite needs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
app compared Axiom statuses with net/http constants, which breaks the rule that app never imports net/http; the codes are plain numbers now. The proxy container got KEEL_PROXY_REPORT_URL and KEEL_WORKER_TOKEN, but keel proxy reads neither: serve pushes the report URL and token in the config. serve's KEEL_OTLP_URL keeps the KEEL_ADDR guard the proxy block used to carry. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A node's own HOST, PORT or URL row is offered as <NODE>_<KEY>, like its provided keys, so picking api.PORT no longer fills in web's own PORT. - The Variables tab reads its two suspense queries with useSuspenseQueries, so they no longer wait on each other. - The bottom panel's error boundary resets on a tab switch too. - check-comments.sh no longer counts a line starting with a dereference (`*x = ...`) as a comment. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
ℹ️ No regressions in the new commits. There's one question for you below: what re-running
install.shnow does on a server that still runs the Convex-era Keel.
Reviewed changes
This review covers the 142 commits from 50547c7 to 545240f. The merge base is still 36c2ded. CI's go, web, swarm and install checks pass on 545240f. These also pass locally: go vet, go test ./..., tools/nocomments and the openapi.json drift check. Separate passes compared nine areas against 50547c7: auth, SQLite, deploy, ingress, observability, the agent, the CLI, realtime and jobs, and the dashboard's wire types. Apart from the deliberate removals below, none found a different outcome on any reachable path.
- Dropped the Convex upgrade path. These are gone:
keel import-convexand the importer, Better Auth scrypt verification, the export and import step ininstall.sh,KEEL_SKIP_CONVEX_IMPORT,/config.js, the CLI's--convex-*flags and its Convex-era discovery hint, and theconvexUrl,convexSiteUrlandconvexImport*fields in the install JSON. A newCLAUDE.mdrule bans backfills and compatibility code. - Banned comments in Go.
tools/nocommentsparses every Go file in CI and inmake test.check-comments.shnow counts TypeScript only. - Moved generated code. sqlc output is now in
internal/gen/sqlc. Kubb, the route tree and varlock output are inapps/web/src/gen. - Replaced the JavaScript emulation with plain Go.
- Typed Axiom rows replace
observability_jsvalues.go. Rows of an unexpected shape are skipped with a warning. - The golden-file changes come from edited inputs or new scenarios. APL text and scoping are unchanged.
- Lengths are counted in runes, and database URLs are built with
net/url.
- Typed Axiom rows replace
- Unset values are zero, not NULL.
0001_init.sqlmakes optional text columnsNOT NULL DEFAULT '', and unset times and revisions default to0.- The endpoint unique indexes are now partial (
domain <> '',public_port <> 0), so blank values don't collide. 0002_canvas.sqlwas folded into0001_init.sql. Thesettingstable anddeployments.shawere dropped.- The API keeps
omitempty, so unset fields look the same on the wire.
- Typed the wire. Ports, replicas and
tailare integers inopenapi.json. Node type, engine, statuses and roles use domain types, with the same enum values. - Narrowed invalidations. A canvas write publishes
/api/environments/<id>and/api/nodes/instead of one topic per node. The dashboard's prefix match covers the same queries. - Rewrote the agent.
- Sinks are keyed by their full config, and events are posted as typed arrays.
/worker/eventsnow accepts only a JSON array. - State now lives in the
keel-agent-statevolume. - Delivery checkpoints, retries and event order are unchanged.
- Sinks are keyed by their full config, and events are posted as typed arrays.
- Made some inputs stricter. Unexpose now rejects a malformed selector instead of treating it as a no-op.
/proxy/eventsrejects an empty name.
ℹ️ Re-running install.sh on a Convex-era server now starts an empty Keel without warning
The README's upgrade path is "re-run the install command". On a server running main's Convex-based Keel, the new installer does the following:
- It overwrites
compose.yml. - It rewrites
.envwithoutINSTANCE_SECRET,BETTER_AUTH_SECRETandCONVEX_SELF_HOSTED_ADMIN_KEY. --remove-orphansremoves the oldbackendandwebcontainers.- It starts an empty control plane and tells the user to sign up, with no warning.
The data stays in keel_convex-data, but the old compose file and secrets are gone. The code at 50547c7 said a rollback needs those secrets. The old keel-worker service also keeps running against a backend that no longer exists. The deployed services keep running, but Keel no longer manages them, and exposed services lose their public endpoints. The PR description still lists keel import-convex, scrypt sign-in for imported accounts, and a Convex upgrade in its test plan.
Technical details
# A Convex-era upgrade now starts empty without saying so
## Affected sites
- `install.sh:158` `save_state`: `.env` is rewritten with only the new keys, so the first run of this installer deletes the Convex-era secrets an earlier install saved.
- `install.sh:151` `write_state`: `compose.yml` is overwritten without the old `compose.convex.yml` copy.
- `install.sh:195` and `:248`: `--remove-orphans` removes the old `backend` and `web` containers. Nothing checks for `keel_convex-data` or a running `keel-worker`, and the closing message says to sign up.
- PR description: the `keel import-convex` row, "imported accounts keep their passwords", "Upgrade a Convex install" in the test plan.
## Required outcome
- Whoever re-runs the installer on a Convex-era server knows before anything is replaced that its data won't come along.
## Open questions for the human
- Will any Convex-era install (for example the Asgard copy in the test plan) run this installer? If so, should `install.sh` stop when `keel_convex-data` exists, with a `die` that names the volume, or is a README note enough?
- Should `.env` keep the Convex-era secret lines it doesn't write, so a rollback stays possible?claude-opus-5-5 | 𝕏
There was a problem hiding this comment.
✅ No new issues found. The one new commit fixes the four open findings from earlier reviews: row naming, the suspense waterfall, the tab reset and the comment regex. All review threads on the PR are now resolved.
Reviewed changes
This review covers 532490c, the one commit since the review of 545240f. CI's go, web, swarm and install checks pass on it. These also pass locally: go test for internal/app and internal/domain, check-types for packages/ui and apps/web, oxlint --type-aware (0 errors) and scripts/check-comments.sh.
- Row keys get prefixed names. A source's own
URL,HOSTorPORTrow now goes throughdomain.SuggestedKey, the same as its provided keys. So pickingapi.PORTfills inAPI_PORTinstead of overwriting the service's ownPORT. Other row keys keep their names, becauseSuggestedKeyonly prefixes those three.connectionSuggestionstill only reads provided keys, so suggestions are unchanged. - Variables load in parallel. The tab reads both lists with one
useSuspenseQueriesover the generated…SuspenseQueryOptions. Their query keys and fetchers match the…Suspensehooks they replace, so the path-prefix invalidations still reach them. - Errors clear on a tab switch. The bottom panel's
ErrorBoundaryaddsresetKeys={[tab]}, so an error on one tab no longer stays up after switching to another. It still resets throughuseQueryErrorResetBoundary. - Pointer lines don't count as comments.
check-comments.shnow counts*only when whitespace,/or the end of the line follows it. Every file's count still equals its.comments-baselineentry, so no file gains room for new comments.
claude-opus-5-5 | 𝕏
Re-running the installer there replaced compose.yml and .env, removed the old backend and web containers and started an empty control plane without a word: the deployed services kept running unmanaged. With the keel_convex-data volume present it now dies before changing anything, and the fix line says how to remove the old install once the services are off it. It reads no old data; there is still no upgrade path. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
[Claude] On the install question: install.sh now stops before changing anything when the keel_convex-data volume exists, and its fix line says how to remove the old install (766227c). There is still no upgrade path from the Convex-era Keel, by design. The PR description no longer mentions keel import-convex, scrypt sign-in or the Convex upgrade test. |
There was a problem hiding this comment.
✅ No new issues found. The new commit answers the last review's question about Convex-era servers: the installer now stops before it changes anything.
Reviewed changes
This review covers 766227c, the one commit since the review of 532490c. bash -n and shellcheck -S warning pass on install.sh. CI's go, web and swarm checks pass on this commit, and install was still running when I checked.
- Installer stops on a Convex-era server.
ensure_dockernow exits with an error when the volumekeel_convex-dataexists. The check runs right afterpreflight, before Tailscale,write_stateor anycomposecall, socompose.ymland.envare left alone. The volume name matches main's compose file (name: keel, volumeconvex-data). Thefix:line repeats main's uninstall steps (compose -p keel down,service rm keel-worker), and-p keel downworks without a compose file in the current directory. If oldsvc-*services are left running, the new Keel ignores them: they carry the samekeel.servicelabel, but the full sweep only looks up nodes in its own database and never removes services it doesn't know. No CI job creates that volume, so theinstalljob doesn't hit the check. - README documents it. The upgrade section now says the Convex-era Keel doesn't upgrade and that the installer stops on a server that still has the volume. That keeps
install.shand its README contract in sync.
One thing outside the code: the PR description's "Not verified, or known gaps" section still says re-running install.sh on a Convex-era server "starts an empty control plane". Since this commit, it stops instead.
claude-opus-5-5 | 𝕏

Experiment: Keel's backend rewritten in Go as one
keelbinary. It replaces self-hosted Convex, Better Auth, the Bun worker and the keel-proxy image. The dashboard and the CLI now run on its HTTP API. Durable workflows are left for a later run, as agreed.What the binary is
keel servekeel-webkeel proxyapps/proxyimagekeel agentkeel-agentSwarm global service, created by serveapps/worker+scripts/deploy-worker.shkeel login,ship, …apps/cli(output contract unchanged)serveandproxystay two containers on purpose. The process that parses internet traffic never holds the Docker socket.Stack
net/http.openapi.jsonis generated from the handlers and committed; CI fails when it drifts.Keel-Invalidateheader, so a mutation sees its own write.modernc.org/sqliteandsqlc, with embedded migrations, one writer and a read pool.domain(pure rules),app(use cases and the ports they need),adapters(sqlite, swarm, caddy, axiom, realtime, jobs, password),transport/http. Contract:docs/go/ARCHITECTURE.md.Recoverpass re-observes Swarm, re-arms timeouts, re-queues applies and resyncs the proxy.KEEL_TS_AUTHKEY). Swarm already needs hosttailscaled, so it isn't the default.How it was built
docs/go/spec/holds seven porting specs written from the TypeScript, plus a completeness pass. Seven backend areas were then built in parallel, each reviewed against its spec, and merged with integration fixes. The CLI and the dashboard followed on the generated API.Then the port was made idiomatic. There is no Convex migration (Keel has no users), so parity with the TypeScript stopped being a goal:
JSONObject, nojs*helpers, no JS string lengths or whitespace. Axiom rows, Docker events, proxy reports, Caddy config and worker config decode into typed structs.NOT NULL DEFAULT 0/'', integer fields are integers on the wire, and runtime state lives onAppinstead of package-level maps.tools/nocommentsin CI).Verified
go build,go vet,go test ./...pass;GOOS=darwin go vet ./...is clean. Area tests cover every exact algorithm: status, task summary, reconcile, variable references, port allocation, the Caddy config builder, the device flow, log shipping.swarm): serve and agent in one process against a real Swarm. It covers deploy, a variable shipped as an update (checked in the service spec), stop, start, redeploy and delete. It passes locally against an isolated Docker-in-Docker Swarm.Not verified, or known gaps
install.shon a server that runs the Convex-based Keel starts an empty control plane; its data stays inkeel_convex-data.keel proxyare unit-tested only, not with real ACME.tailscale serve) this doesn't apply.-tags keel_noproxybuilds a lean CLI.Test plan
go,swarm,web,installjobs greeninstall.shon a VM, then deploy, expose over https, Sign in with Axiom🤖 Generated with Claude Code