Architecturesource: AGENTS.md · docs/adr

Architecture

DOP is three repositories that ship and one that pins them. The components are independent git repositories, aggregated as submodules by the umbrella repository, which carries the product’s documentation: the PRD, the ADRs, the specs and the API collections.

The components

  • dop-coreGo
    The core: domain, state, transactions and the event log.One binary, four modes (serve, worker, sched, launcher). It owns the schema and the vault; it is the only source of truth, and it verifies every caller's signature.
    dop-core ↗
  • dop-apiPython
    The BFF: REST and SSE for the cockpit, gRPC for the CLI.It has no database and no secret. It translates and forwards; every write goes through the core.
    dop-api ↗
  • dop-appReact · Vite
    The cockpit where the developer works.The attention box, the tree of workspaces and projects, the demand's cockpit. It consumes a committed contract and generates its hooks and schemas from it.
    dop-app ↗
  • dop-infraTerraform · k3d
    The platform's infrastructure.Terraform for Google Cloud (Cloud Run, Secret Manager, Identity Platform) and a local k3d environment with Postgres, NATS JetStream and the Firebase emulators.
    dop-infra ↗
  • sandboxmicroVM
    One per demand, with a single shared worktree.The hard boundary is between accounts and between demands. The project's knowledge is cloned inside as a git repository; verification runs from source in an ephemeral runner.

The diagrams

Six pictures, from the whole platform down to each component. Every name in them is a package, a port or an adapter that exists in the repositories; what is planned and not built is drawn dashed.

The platform

The DOP platform's architectureThe cockpit talks REST and SSE to the BFF, and the CLI talks gRPC to it; the BFF talks gRPC to the core; the core owns Postgres for state and NATS JetStream for events, and provisions one sandbox per demand. Identity Platform signs the user in at the edge.Identity Platformsign-in · tokensSandboxone microVM per demandCockpitdop-app · ReactCLIdop-cliBFFdop-api · PythonCoredop-core · Gothe only source of truthschema · vault · event logPostgresstate · outboxNATSJetStreamREST · SSEgRPCgRPCstateeventsprovisionsverifies
The cockpit and the CLI reach the BFF; the BFF reaches the core; the core owns the state, the events and the sandboxes. Identity Platform signs the person in at the edge.

The core, inside — the hexagon

The core, inside: ports and adaptersOne binary with four modes drives the domain from the left; the domain reaches infrastructure only through ports; each port has two or more adapters on the right, verified by one contract suite.drivesservegRPC · app/grpcworkerconsumersschedperiodic taskslaunchersandboxesevery call verified: a person's token,or a platform assertion (ADR-0022)domainidentity · hierarchy · resourcedemand · workflow · executiondelivery · attention · notificationreaction · event · knowledgecost · agent · secondfactorservices, entities, rules —no SQL, no HTTP, no SDKevery write emits an eventportsdriven — one port, its adaptersRepositoriesPostgresEventBusNATS JetStream · memoryIdentityProviderFirebase · OIDCSecretStoreSecret Manager · k8s · memoryObjectStoreGCS · fsSandboxLauncherDocker · KubernetesVerificationRunnerDocker · Kubernetes · scriptProjectRepositorygit server · local · remoteMailerOneSignal · SendGrid · SMTPSMSerTwilio · ZenviaAgentProviderAnthropic · OpenAIGitProviderGitHub · GitLabone contract suite per port; every adapterpasses the same tests (ADR-0001)
One binary, four modes, one domain. The domain never imports a driver: it declares a port, and the adapter on the right implements it. Every port has at least two adapters and one contract suite that both must pass — which is what makes the local environment and Google Cloud the same platform.

The BFF, inside — two transports, one rule

The BFF, inside: two transports, one ruleREST with SSE and gRPC are adapters that call the same use case; the use case talks to the core over gRPC with a platform assertion, and verifies the person's token with Identity Platform. It holds no database and no secret.drivesREST · SSEFastAPI · app/routersgRPCapp/grpcapithe rule lives once, in usecases;both transports call the same function,and a parity test proves itusecasesidentity · hierarchy · resourcedemand · workflow · executiondelivery · attention · costknowledge · runtime · streamsecondfactortranslate, authorize, forward —nothing persists hereplatformsecurity · context · errors · loggingdrivencoreclient → dop-coregRPC · TLS · assertion (ADR-0022)Identity Platformverifies the person's token · resolves the accountFirebase Adminmints action links (e-mail verification)no database · no secret · every write goes to the core
REST with SSE for the cockpit, gRPC for the CLI, and the same use case underneath. Authorization is pinned to the use case, not to the router, so a rule cannot exist on one transport and not the other; a parity test keeps it that way.

Events and the flow engine

Events and the flow engineA write commits its event with the state; a relay publishes it to NATS JetStream; every consumer receives it. The flow engine and the failure path are two sub-processes, each drawn in its own figure: the engine decides what an event triggers; the failure path covers every consumer's delivery.writePostgres · txoutboxsame txrelayat-least-onceNATS JetStreamdop.> · dlq.>Envelopeaggregate · actor · requestdispatcherplannedtimelineprojectionattentionprojectionnotificationMailer · SMSeron failure, any consumerthe flow engineADR-0010 · Decide → PlannedAction → Executesub-process ↓+the failure pathADR-0014 · NAK ×5 → dlq.event → event_errorssub-process ↓+
The pipeline: a write commits its event in the same transaction; a relay publishes it; every consumer receives it. The two blocks marked with a plus are sub-processes drawn in the next two figures — the flow engine, which decides what an event triggers, and the failure path, which covers every consumer's delivery, not one of them.

The flow engine

The flow engineA flow is a versioned composition of typed stages; each stage has a gate, artifacts and actions on enter and exit. The effective flow is resolved down the chain platform, account, workspace, project, demand, and frozen when the demand starts. A stage-advanced event is decided from the flow's stage actions and from the accumulated rules table into planned actions, which the executor runs through a registry of four actions, recording each as applied per event, rule and action.Flowname · versioncontextspecplanimplementationtesthuman_validationfinalizationStagetypegate · human|noneartifactsactions · enter|exitdemand/‹id›/‹kind›.md · ADR-0021resolution chainplatformaccountworkspaceprojectdemandnearest winsfrozen at demand startreaction_rulesaccumulate down the chaintrigger · actions · disablesstage-advanced · from → toDecideStagefrom the stage's actionsDecidefrom the rules tablePlannedActionrule · name · paramsExecuteRegistry · Appliedopen_attentionclose_attentionsend_emailprovision_benchapplied — idempotent(event, rule, action)
A flow is data: a versioned list of typed stages, each with a gate, its artifacts and its actions on enter and exit. The effective flow comes down a chain — platform, account, workspace, project, demand — where the nearest declared level wins, and a demand freezes the version it started with. When a stage advances, the stage's actions and the accumulated rules table are decided into planned actions; the executor runs them through a registry of four and records each as applied per event, rule and action, so a retry never repeats one.

The failure path

The failure pathA consumer handles a delivery; on success the message is acknowledged and the success recorded; on failure it is redelivered with backoff up to five times. An exhausted delivery becomes a dead letter carrying the whole envelope, the consumer and the attempts, published to dlq.event with a message id of event and consumer. The DLQ consumer re-runs the same handler up to three more times; a success records; a failure terminates the message. Every failure is classified by its signature, learned after two exhaustions, and written to the error ledger.NATS JetStreamdop.>consumerHandle(event)ok?yesAckRecordSuccessno · NAKredeliver1s · 5s · 15s · 1min×5 · MaxDeliverexhaustedDeadLetterevent · consumer · attemptsdlq.eventMsgId event:consumerDLQConsumersame handler · ×3ok?yesno · TermClassifySignature(consumer, code|kind)recoverableirrecoverableunknownerror_signatureslearned · ×2event_errors(event_id, consumer) · attempts · broker_attempts · classification · last_success_at
The same path covers every consumer. A failed delivery is redelivered with backoff up to five times; an exhausted one becomes a dead letter with the whole envelope and its attempts, published to its own subject. The dead-letter consumer re-runs the same handler three more times, then terminates the message — but every attempt, its classification and the last success are kept in the error ledger, and a signature that exhausts twice is learned as irrecoverable until a success demotes it.

The cockpit, inside

The cockpit, insideA router mounts the pages; pages are built from component families; components read and write through hooks; hooks call a generated client — react-query hooks and Zod schemas produced by Orval from a committed OpenAPI document — whose fetch layer carries the person's token and the account. Firebase Auth issues the token; the BFF answers REST and pushes SSE.routerwouter · App.tsxpagessign-insign-uponboardinghomeworkspaceprojectdemandcard-executionaccountinvitecomponentsplatformlayout · sidebarattention boxlivesecuritygate · step-upstage viewsplan · exec · testdoc viewermarkdownuishadcnuiStorezustandi18npt-BR · enhooksuse-apireact-queryuse-attention-streamSSE · EventSourcethe generated contractapi-client-reacthooksapi-zodschemascustom-fetchAuthorization · x-account-idopenapi.jsonlib/api-spec · committedOrvalIdentity Platformauth.qa.dop-t.comFirebase Auth SDKsign-in · id tokenid tokenRESTSSEBFFapi.qa.dop-t.com · dop-api
A router, ten pages, six component families, two hooks, and a generated client underneath. The contract is a committed OpenAPI file; Orval produces the react-query hooks and the Zod schemas from it; one fetch layer carries the person's token and the account on every call. Firebase Auth issues the token; the BFF answers REST and pushes the attention stream over SSE. Nothing the backend already decided is decided again here.

How the core verifies its callers

How the core verifies its callers (ADR-0022)The person signs in at Identity Platform and the cockpit holds the token. The cockpit sends it to the BFF as a bearer. The BFF forwards it whole to the core and adds a signed assertion naming caller, actor, kind, account, session and expiry; on Cloud Run it also carries the invoker token. Callers with no person send the assertion only. The core's interceptor verifies the token through the identity port, verifies the assertion's HMAC with the caller's key, refuses when token and assertion name different actors, applies the mode, and leaves the decision to authorization. Transport authentication — Cloud Run IAM or a network policy — is additional.transportCloud Run IAM · NetworkPolicyIdentity Platformsign-inid tokencockpitdop-appBearerBFFdop-apitoken + assertionworker · schedlauncherassertioncoreinterceptorverify tokentoken · IdPverify HMACHMAC · 2 minsame actor?else: refusedmodestrict · permissive · offauthorization
Every call to the core carries a signature the core can check: the person's token, forwarded whole by the BFF, or a platform assertion signed with the caller's own key when there is no person. The interceptor resolves the actor from the token and the account from the assertion, refuses when the two disagree, and lets authorization say no with a message that means something. Cloud Run IAM in the cloud and a network policy on a cluster restrict who can reach the service at all; neither replaces the signature.

The platform and what it talks to

The platform and what it talks toDOP runs on Google Cloud; keeps its own Postgres and NATS; sends e-mail and SMS through OneSignal, SendGrid, SMTP, Twilio or Zenvia; reads and writes repositories on GitHub and GitLab; runs agents on Anthropic or OpenAI; and provisions sandboxes on Docker or Kubernetes. Jira is planned.DOPcore · BFF · cockpitGoogle CloudCloud Run · Secret ManagerCloud Storage · Identity PlatformFirebase HostingcommunicationOneSignal · SendGrid · SMTPTwilio · ZenviaMailer · SMSerdata, oursPostgres · pgvectorNATS JetStreamsandbox runtimeDocker · KubernetesSandboxLauncherVerificationRunnercode hostsGitHub · GitLabGitProvidermodel providersAnthropic · OpenAIAgentProviderJiraplanned
Every external service sits behind a port, so swapping one is an adapter and a configuration value, not a rewrite. What is solid has an adapter today; what is dashed is on the roadmap.

The five invariants

The architecture's load-bearing walls. A change that breaks one is wrong even when it compiles and the tests pass.

  1. The core is the only source of truth.It owns the schema and the vault. Every write goes through it.
  2. The BFF has no database and no secret.It translates and forwards; it never persists and never holds a credential.
  3. The .proto files are the contract.Generated code is regenerated, never hand-edited. An incompatible change is refused on purpose.
  4. The cockpit consumes a committed contract.The OpenAPI spec is downloaded and committed; the react-query hooks and the Zod schemas are generated from it.
  5. The core verifies its callers, and verification runs from source.A signature on every call, never a trusted header. A verification run builds from the repository in a runner, not from an image.

The life of an event

Every write in the core emits an event in the same transaction. What happens next is infrastructure, not the repository's problem.

  1. writeA transaction in Postgres changes state and appends the event to the outbox — one commit, or neither.
  2. outboxA relay reads the outbox and publishes to NATS JetStream. The message id is the event's id, so a retry cannot duplicate it.
  3. envelopeThe event travels with its context: aggregate key, actor, request id, session id, caller.
  4. consumerA consumer processes it. On failure it is redelivered with backoff, up to five times.
  5. dead letterExhausted, the event goes to one dead-letter queue with the attempt history and its classification: recoverable, irrecoverable, unknown.
  6. error ledgerEvery failure is recorded per (event, consumer); a signature that burns every retry twice is learned as irrecoverable, and a success demotes it.
MaxDeliver 5 backoff 1s · 5s · 15s · 1min dedup key (event, consumer)

The decision record

Every structuring decision, in the MADR format: context, decision, alternatives considered, consequences. One subject, one ADR.

  1. 0001 Infrastructure behind ports with pluggable adapters
  2. 0002 Tenancy: the account owns everything, and an organization proves itself by domain
  3. 0003 An organization credential to act, human authorship on the commit
  4. 0004 The demand is an event log; everything else is a projection
  5. 0005 No green, no PR: native verification, and a merge queue per repository
  6. 0006 Context is a subsystem: a knowledge base and a package per demand
  7. 0007 Multi-agent per demand: addressable threads and published findings
  8. 0008 The LLM's cost: measuring it, capping it, routing it, and spending less
  9. 0009 The resource as the account's unit of ownership and sharing
  10. 0010 A dynamic, typed and inheritable workflow
  11. 0011 The project orchestrator: the techlead agent
  12. 0012 A core in Go, a BFF in Python, and the boundary between them
  13. 0013 The .proto is the contract's source of truth
  14. 0014 PostgreSQL as the single database, and the outbox that moves its events
  15. 0015 Firebase emulators in the local environment; Terraform as the single owner
  16. 0016 The agent runtime: a port per vendor, running inside the core
  17. 0017 A microVM per demand, with a single shared worktree
  18. 0018 Communication: the trigger and the channel are born together
  19. 0019 The invite has no secret: identity in place of a bearer
  20. 0020 The second factor is the platform's, with three verifiers
  21. 0021 The project's knowledge is a git repository, hosted by the platform
  22. 0022 The core verifies a signature; it does not believe a header
  23. 0023 Verification builds from source in a runner, not from an image