Repository navigation
Design Decisions
Architecture Decision Records (ADRs) for CronCompose. Each records the options considered, the choice made, and the consequences. Source of truth for the narrative lives in the repo docs (docs/architecture.md, docs/security.md, docs/agent-protocol.md); this page is the indexed summary for wiki readers.
See also: Diagrams · Architecture · How it Works
Status: Accepted
Context: Jobs must keep firing on intermittently connected hosts (home Pis, NAT, flaky links).
| Option | Summary |
|---|---|
| A. Central scheduler | Control plane evaluates cron and pushes “run now” at fire time |
| B. Agent-local scheduler (chosen) | Control plane owns definitions; each agent caches them and evaluates cron locally |
Decision: Option B.
Consequences:
- Agents mint run ULIDs; control plane upserts by id (idempotent replay).
- Schedules carry IANA timezones; DST is handled on the machine that runs the job.
- Catch-up policy (
once/all/skip) decides what happens after a long offline period. - “Run now” still works as a live-stream command.
Diagram: Job definition and log data flow
Status: Accepted
Context: Targets often sit behind consumer routers with no inbound ports.
| Option | Summary |
|---|---|
| Inbound SSH / agent listener | Requires port forwarding or public IP |
| JSON WebSocket channel | Simple to proxy; hand-rolled envelope |
| gRPC bidi stream over mTLS (chosen) | Typed protobuf, one persistent connection, identity + encryption together |
Decision: Agents always dial out on a single AgentStream. Enrollment is the only agent REST call (POST /api/v1/agents/enroll) so cert bootstrap does not require gRPC first.
Consequences:
- No CronCompose inbound ports on targets.
- Protocol can map to WebSocket later if self-hosters need it; message set is transport-agnostic enough.
- Edge / reverse-proxy modes exist for environments that cannot end-to-end mTLS to
:9090.
Diagram: Agent enroll, sync, and run
Status: Accepted
Context: Operators need live run output in the browser.
Decision: Agents push LogChunks up gRPC; LogBroker fans them to GET /api/v1/runs/:id/logs/stream (SSE).
Why SSE: one-directional, auto-reconnects, trivial to serve from Fiber.
Exception: the Web Terminal uses WebSocket because it needs bidirectional byte-accurate I/O.
Status: Accepted
Context: Operators edit scripts; history must show what actually ran.
Decision: Editing a script inserts a new job_version and bumps current_version_id. Every run records the version id. Connectors mirror the idea with config snapshots before every write.
Status: Accepted
Context: Jobs need API keys without leaking them in UI, logs, or script files.
Decision:
- AES-256-GCM envelope encryption at rest.
- No API endpoint returns secret values.
- Control plane decrypts only when building
SyncJobsand sends secrets over mTLS. - Agent passes secrets as env vars at exec time; never interpolates into the on-disk script.
See Security.
Status: Accepted (for now)
Context: Multi-tenant SaaS would reshape auth, billing, and isolation.
Decision: One deployment = one tenant. Roles (owner / admin / operator / viewer) live inside that tenant. An org_id can be added later without reshaping core tables.
Non-goals retained: Windows agents, general CI/CD, multi-step DAGs (see Roadmap).
Status: Accepted
Context: Operators want nginx / systemd / Docker / pm2 without a second agent protocol.
Decision: Connectors are additive: discovery, config edit (validate → backup → apply), lifecycle actions, and object logs all ride the existing outbound stream, audit log, and RBAC. No new inbound path.
See Connectors.
Status: Accepted
Context: Some hosts need privileged connector or install actions; default must stay unprivileged.
Decision:
- Default agent user is unprivileged.
- Passkey step-up can elevate via a narrow
agent-privctl elevate|demotesudoers path (neverNOPASSWD: ALL). - Root install (
AGENT_RUN_AS_ROOT=1) is recorded on the server row so the UI matches reality.
See Agent Root Access.
Status: Accepted
Context: Two deploy projects can both ship an app named web.
Decision: Deploy process names are qualified as <project>-<app> (unless already matching) so pm2 / systemd / docker names do not collide on one host.
| Topic | Page |
|---|---|
| Topology and components | Architecture |
| End-to-end operator loop | How it Works |
| Protocol messages | Agent Protocol |
| Schema | Data Model |
| Threat model | Security |
| Visual maps | Diagrams |