Core vs. Plugin Boundary
Circus is deliberately split into a domain-agnostic core and one or more domain-specific plugins. The core knows nothing about what you're building — videos, leads, documents — and a plugin supplies exactly that. The value of this split lies less in the division itself than in the strict, enforced boundary between the two halves, which is what lets the platform be extended without becoming entangled.
What the core owns
The core provides everything that holds regardless of domain: authentication, sessions, and API keys; the model, agent, prompt, service, and workflow configuration registries; the workflow execution engine, execution logs, and cost tracking; the background reconciler; the workspace lifecycle as a status-driven state machine; system context and system-wide configuration; the plugin registry and startup validation; and the core UI shell — layout, dashboard, and cost analytics.
What a plugin owns
A plugin supplies the domain: its entity object table and entity-specific fields; the entity-to-workspace join table, carrying its own is_deleted / is_archived flags; its entity-specific workspace statuses; entity-level pointers such as active and final workspace; any additional domain tables; its list and manage-entity pages; and its manifest, which declares the plugin's identity, routes, and four extension points (result handler, workspace snapshot provider, dashboard stats provider, workspace display resolver).
The invariants
The boundary rests on a small set of rules — most guaranteed by the structure itself, one upheld by discipline:
- The core never reads from or writes to plugin-owned tables. It has no knowledge of their shape, so this is structural rather than a policy it has to remember.
- Dependencies point one way. Plugin tables may reference core tables (
workspace,workflow_execution) via foreign keys; core tables never reference plugin tables. This is a fact of the core schema. - Core workspaces are never deleted. There is no
is_deletedoris_archivedon the core workspace table — deletion and archival are plugin concerns on the join table — so execution history and financial data are preserved by design. - Plugins should reach core state only through core services, with a single startup exception: seeding their
entityandworkspace_statusrows. This last one is a contract, not an enforced guarantee. A plugin runs with full database access, so nothing mechanically stops it from issuing raw SQL against core tables; the platform relies on the plugin developer's discipline here — and, since the core and a plugin are frequently built by the same person, ultimately on their judgment. Honouring it keeps the platform's guarantees intact; bypassing it forfeits them.
A consequence worth stating plainly: the core is also storage-agnostic. On completion it delegates domain-artifact persistence to the plugin's result handler and owns none of the resulting assets.
Why it's built this way
The boundary buys three things at once. Extensibility: a new domain is a new plugin, with no core changes. Safety: the core's own code structurally never touches plugin data, and a plugin that honours the contract never touches core data — so a well-behaved plugin cannot corrupt core execution or financial state. Durable analytics: because financial reporting is derived entirely from core-owned execution logs, plugin-level deletion or archival never distorts it. The component diagram and the ER diagrams elsewhere in this package show the same boundary structurally.