Skip to content

Design Decisions

Yash Aryan edited this page Oct 9, 2026 · 2 revisions

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


ADR-001 — Agent-local scheduler (not central)

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


ADR-002 — Outbound mTLS gRPC (not inbound SSH / WebSocket-first)

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


ADR-003 — Live browser logs via SSE (not WebSocket)

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.


ADR-004 — Immutable job versions

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.


ADR-005 — Secrets: envelope encryption, write-only API, inject at sync

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 SyncJobs and sends secrets over mTLS.
  • Agent passes secrets as env vars at exec time; never interpolates into the on-disk script.

See Security.


ADR-006 — Single-tenant self-host first

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).


ADR-007 — Connectors reuse the agent stream

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.


ADR-008 — Agent root is explicit and narrow

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|demote sudoers path (never NOPASSWD: ALL).
  • Root install (AGENT_RUN_AS_ROOT=1) is recorded on the server row so the UI matches reality.

See Agent Root Access.


ADR-009 — Process names are project-qualified

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.


Related reading

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

Clone this wiki locally