Source Of Truth
src/app/api/v1/**is the public backend surface for web, mobile, desktop, and extension clients.src/server/app-api/v1/**holds the route orchestration behind those Next route wrappers.src/server/**holds server-only auth, billing, storage, AI, tools, route services, and provider adapters.src/shared/**holds isomorphic contracts and client-safe helpers.src/contexts/**holds app-wide client providers (AuthContext,WorkspaceContext);src/hooks/**holds app-wide client hooks (useWorkspaceChanged). Both may import@/sharedonly.src/features/**holds web feature containers and feature-local helpers. Cross-feature imports are banned; shared primitives belong in@/shared,@/contexts, or@/hooks(e.g. the workspace provider, routing helpers, and types that used to sit insidefeatures/workspaces).src/server/workflows/**holds Vercel Workflow SDK durable orchestration (automation-run,automation-schedule,personal-chat-work,workspace-agent-turn). Workflow bodies bundle for the workflow runtime — keep their imports to"use step"functions and@/alias modules only.convex/**owns durable app state, domain mutations, scheduled work, and Stripe webhook handling.apps/admin/**holds the standalone operator console (seeinternal/develop/admin-console.md): its own Next.js app, WorkOS sign-in, session cookie, and a singleconvex-adminmodule that reaches theadmin/console:*Convex functions withADMIN_API_SECRET.apps/adminmay not importsrc/**andsrc/**may not import it back.scripts/is split by purpose:scripts/ci/(checks, boundary rules, baselines consumers),scripts/release/(hosted build wrappers and trusted PR preview lifecycle control),scripts/qa/(smoke tests, rehearsal/readiness harnesses),scripts/db/(migrations, backfills, ops),scripts/lib/(shared script helpers).scripts/dev-setup.shstays at root. The retired staging ignore-build script is removed;overlay-landinguses native Git deployment and the hosted build wrapper.packages/overlay-app-core/**holds shared cross-surface contracts and UI settings types.packages/overlay-api-client/**holds typed transport wrappers for/api/v1/**.packages/overlay-ui/**holds the shared presentation primitives (buttons, inputs, dialogs, and the list-page system:Tile/TileGrid/TileIcon/TileSkeleton/CreateTilefor grid tiles,ListRowfor list rows, andHeaderSearchfor collapsible header search). List pages (Files, Agents, Extensions) must render their tiles, rows, and headers through these primitives instead of hand-rolled markup so spacing, radii, hover language, and dark-mode surfaces stay uniform.packages/overlay-agent-bridge-protocol/**owns the vendor-neutral, versioned Zod wire protocol between Overlay and connected execution environments.packages/overlay-agent-host/**is the portable outbound-only host executable. It owns local device keys, SQLite delivery state, filesystem grants, and fake/ACP adapters; it does not own workspace identity, transcripts, approvals, policy, or billing.packages/overlay-sandbox-runtime/**is the provider-neutral managed execution boundary. It owns sandbox lifecycle, command/file/port/snapshot/network/usage contracts and provider translations (Box today, E2B planned). Provider SDK types and raw handles do not cross into public routes, billing, or connected-agent persistence.
src/server/app-data/capabilities.ts
publishes the Convex capability set and src/server/app-data/repositories.ts
constructs the Convex repositories every domain service consumes. The bespoke
Postgres repository layer, Drizzle schema, app-data migrations, and parity
harness were removed; enterprise self-hosting runs the official
convex-backend binary (see internal/develop/convex-only-self-hosting.md).
Connected-agent persistence goes through the Convex-backed
ConnectedAgentRepository. Environment, binding, command, remote-session,
approval, sandbox-lease, enrollment, proof-challenge, short-lived credential, and
request-replay records live in Convex tables.
Remote output advances a per-session contiguous event cursor transactionally;
it projects into the existing conversation and AgentRun lifecycle rather than
creating a second transcript.
Negotiated connected-agent text streams persist in bounded remoteMessageTextChunks
owned by the existing assistant message. The message holds stable text references and
a short preview cache; authorized transcript, room, context, share and knowledge reads
hydrate canonical text before returning it. Storage boundaries do not become Markdown
boundaries. Chunk updates emit conversation events while avoiding full message rewrites,
and hard deletion cascades to the message’s chunks. Legacy rows remain readable without
migration. Liveness heartbeats live on the run, independently of transcript text.
A connected-agent environment is a machine or runtime boundary, not a working-directory or
single-agent boundary. Bindings choose an agent and adapter; each command chooses an explicitly
granted working directory. The host can grant multiple roots or explicit OS-user-wide access.
Protocol versioning, validation, sequencing, enrollment proof canonicalization, and signed HTTP
transport remain in the bridge/host
packages so Convex does not become a public host API. Convex owns a separate TypeScript
path map for its transitive shared imports. Align bridge-protocol source aliases with
the web config so clean backend builds do not require generated package dist files.
ConnectedAgentControlPlaneService is the server-only policy boundary for enrollment and host
traffic. Browser management routes use the normal authenticated BFF and active-workspace
resolution. Host routes are explicit BFF exceptions because they use outbound machine auth:
method-scoped opaque credentials plus Ed25519 request proofs and atomic nonces. Route handlers
never select a provider directly; both modes use ConnectedAgentRepository from app data.
Overlay Cloud machines are provisioned for Computers and, later, for hosted other-agents; the
private runtime is Box. The browser sees Overlay Cloud, an environment, and lease state, not a
provider object.
Clients should not call model providers, billing providers, identity admin APIs, object storage, or Convex directly unless a route explicitly exists for that surface.
src/app/_components/AppShellLayout.tsx is the reusable web shell boundary.
Canonical /app/** routes, ordinary auth flows, and public full-page product
destinations compose it from their route-group layouts. The shell accepts an
explicit showcase mode so public routes do not depend on hidden rewrite query
parameters. Protocol callbacks, integration popups, public share links, and
visual fixtures remain standalone by contract.
Domain Decomposition
Web file upload transport and error parsing live insrc/features/knowledge/adapters/webKnowledgeFilePort.ts; KnowledgeView
composes those ports into the shared surface. Keeping the upload implementation
out of the host preserves the knowledge surface boundary check.
Workspace behavior stays behind the existing service/repository contracts while
large seams are split by responsibility. The WorkspaceSwitcher controller owns
navigation and lifecycle state; WorkspaceSwitcherView owns the trigger, menu,
and dialog presentation.
Personal and organization workspaces use the same collaboration capabilities:
people, agents, direct messages, channels, teams, invitations, and resource
grants all follow the same service and repository contracts. Personal differs
only in lifecycle: it is the account’s permanent default workspace, cannot be
archived through workspace lifecycle APIs, and its creating principal remains
the canonical active owner. Permanent account-deletion erasure is a separate
audited compliance path.
Work (tasks, board, timeline)
work is a content domain: tasks that appear in a List, a Board and a Timeline, in Personal or Workspace scope.
Where things live:
src/shared/work/— the isomorphic rules: statuses and limits (status.ts), fractional string ranks (rank.ts), parent and “blocked by” validation (graph.ts) and who may work on a task (permissions.ts). Convex and the UI import these, so what the UI previews is what the backend enforces.convex/work/workItems.tsandworkItemEvents.ts— theworkItemsandworkItemEventstables. Visibility, move, archive and restore reuseconvex/lib/resourceScope.tsandscopeMutations('workItems', 'content'); the activity log is written byafterApplyand by each mutation. Helpers are inconvex/lib/workItems.ts.- Editing is wider than for most content: any member can work on a workspace task when the workspace lets members edit workspace content, and the assignee always can. Move between scopes, archive and restore stay with the creator and admins, exactly as for other content.
- Dependencies (
blockedBy) are set throughupdate, which reads the tasks they lead to and refuses a loop withcheckBlockedBy(src/shared/work/graph.ts); blockers must be active tasks in the same scope. The Timeline draws them withdependencyArrow(src/shared/work/timeline.ts). - The assignee is a workspace principal (a person now, an agent later). Dates are
YYYY-MM-DDstrings so a due date never shifts with timezone.
Personal Chat execution lifecycle
Interactive Personal Chat runs use the provider-neutralAgentRun contract in
src/shared/agents/agent-run.ts. AgentRun.status is the only persisted
authority for queued, running, approval-waiting, completed, failed, or cancelled
execution; conversationMessages.status is a rendering projection. Convex
creates the assistant placeholder and AgentRun together, and completes,
fails, or cancels both records in one storage transaction.
Chat mode continues to use the AI SDK ToolLoopAgent. It streams directly to
the connected browser, while Next after() drains a tee of the stream for a
persistent conversation. Overlay does not write token deltas for this path and
does not use the Cloudflare chat relay. The final assistant content, parts,
usage, routed model, and terminal AgentRun state are written once when the run
ends. On reconnect, the client polls GET /api/v1/conversations/run and reloads
the final message; a non-terminal run shows the disconnected generating state.
ToolLoop runs tolerate browser disconnection but not a process crash. Their
lease expires to a structured tool_loop_lease_expired failure through the
Convex cron. Stop first cancels AgentRun, then
best-effort aborts the in-process AbortController; late completion cannot
overwrite cancellation.
Work mode uses @ai-sdk/workflow’s WorkflowAgent behind the same AgentRun
contract. The request route performs the same authorization, context and memory
construction, model policy, tool filtering, billing reservation, and user-message
persistence as Chat mode, then starts workflows/personal-chat-work.ts. Model
calls and tool executions are durable Workflow steps; the workflow ID is stored
on AgentRun. Work mode exposes the workflow’s writable stream as a UI message
SSE stream: the WorkflowAgent writes ModelCallStreamPart chunks to the run
stream and the route pipes them through createModelCallToUIChunkTransform. The
client renders tokens, tool calls, and approval requests in real time, then
receives the final persisted message through the normal conversation data path.
Approval-required tools transition AgentRun to waiting_for_approval with a
durable hook token and tool-call summary. POST /api/v1/conversations/run/approval resumes the hook; the workflow returns the
same run to running. Stop cancels both AgentRun and the Workflow run. Internal
Overlay tool mutations receive agent-run:<runId>:tool:<toolCallId> as their
idempotency key. Connector providers outside Overlay must still honor their own
idempotency semantics; Workflow step replay alone cannot make an external API
idempotent.
Both runners write comparable observations onto the terminal AgentRun. Chat
records first-token and completion latency from its direct stream; Work records
first-token latency from the workflow stream, completion latency, and durable
step observations. Provider token usage and
estimated provider cost, tool outcomes and retries, cancellation timing,
browser-disconnect outcomes, lease/process-loss observations, and stale-run
detection feed GET /api/v1/conversations/run/metrics. The report keeps raw
sample counts and denominators and never recommends a runner; product decisions
must be made from the resulting evidence.
Auth
- Browser auth uses the signed httpOnly
overlay_sessioncookie. - Browser
overlayAppClientrequests, and direct app fetches that usefetchWithSessionRecovery(workspace bootstrap and policy reads), recover one same-origin/api/v1/*401 through the sharedbrowser-sessionbroker and retry once after an authenticated refresh. AuthContext and Convex token recovery use that same in-flight refresh and Web Lock. Delayed responses from before a completed refresh do not start another refresh. The retry preserves JSON bodies, workspace headers and idempotency keys; it excludes explicit bearer credentials, omitted cookies, redirects, request/stream bodies, and server-side callers. A failed refresh or second 401 remains a failure; 403 permission denials are never retried. API handlers stay read-only with respect to session rotation. - Native/mobile/desktop auth uses supported bearer access tokens against the same
/api/v1/*backend. - Internal server-to-server calls use short-lived service auth from
src/server/auth/service-auth.ts. - Shared route authentication flows through the API boundary helpers in
src/server/auth/app-api-auth.ts.
/api/v1/* route should reject requests that lack a valid browser session, mobile bearer token, or service auth token.
App Bootstrap
GET /api/v1/bootstrap is the canonical signed-in startup call. It returns user info, entitlements, UI settings, model catalogs, feature flags, navigation destinations, and defaults.
New clients should start from this route and reuse existing API contracts before adding client-specific endpoints.
Billing
Stripe is the hosted billing provider for subscriptions, top-ups, auto top-up preferences, customer portal sessions, and webhook events. Private deployments can disable billing with runtime config. Convex is the durable entitlement/usage source of truth after webhook processing. Billing ownership is represented separately from user identity by an additiveBillingAccount contract. An account has exactly one scope owner: a user for personal, or a workspace for workspace. markup_25_v1 explicitly preserves the current 25% markup policy. New personal accounts start with an empty budgeted balance so account creation never grants spend implicitly. The compatibility migration creates one personal account per user, attaches legacy billing and usage rows, and dual-writes user and account identifiers plus canonical subscription/balance shadows. User-keyed records remain the enforcement and rollback source until balance parity and exact Stripe-subscription verification pass; the migration never switches reads automatically.
Migration balance parity sums only active reserved and reconcile_required rows through the compound billing-account/status index. Historical finalized and released reservations are deliberately excluded, so a high-volume account does not hit the active-reservation safety cap merely because it has more than 1,000 completed operations.
BillingPayerResolver separates workspace authorization from payment selection. The workspace-wallet master flag and off -> internal -> selected -> general rollout select which organization workspaces have switched. Personal workspaces and non-selected organizations retain the compatibility path. Once selected, an organization call requires an active workspace account and has no personal fallback. Workspace reservations update the account-keyed balance and an optional member or programmatic fixed-period limit atomically; the original personal reservation method remains unchanged for rollback.
The app bootstrap and subscription read use that same resolver. Active members of an eligible organization therefore receive the workspace account’s plan and budget for model gating, while mutating billing operations remain restricted to owners and admins. Workspace-scoped clients send the active workspace header explicitly; a member’s personal subscription is not treated as the organization’s entitlement or payment fallback.
All enabled hosted-cost boundaries now pass the resource workspace plus a member or programmatic subject into the resolver. This includes shared chat and named agents, Convex knowledge embeddings, browser/Daytona, hosted media and transcription, gateway search, and automation/API execution. Scheduled automations use automation:{id} and API keys use api-key:{id}. Vercel Workflow overhead is a separate 20-event estimate charged at the provider event rate plus markup_25_v1. Integrations, functions, and MCP execution are bounded to 12 tool steps per agent turn and 50,000 response characters; file ingestion is bounded to 20 requests per user per hour plus the plan storage cap. src/server/billing/billable-feature-coverage.ts is the launch inventory and its test fails when a category lacks metering or a concrete quota.
Chat reservations price the full 12-step model-call envelope, including the
per-step 8,192 output-token ceiling. A tool loop must not reserve only its first
model call: later completed steps would otherwise exceed the reservation and be
quarantined even when provider usage is valid.
Box is the managed sandbox provider (Vercel Sandbox and Daytona were removed; E2B is planned for self-hosting). Agent-environment leases no longer hold a
worst-case 24-hour reservation: ManagedAgentSandboxBilling.reserve takes only a small bootstrap
hold (the low-balance floor plus a short coverage window), and a per-lease meter debits the wallet
periodically while the sandbox runs. Each tick reads provider instance.usage() counters, computes
the delta against a cursor persisted on the lease (usage.meteredUsage + meterVersion), and
commits debit + cursor atomically through meterSandboxLease on the connected-agent repository —
retries replay as lease_conflict instead of double-charging. When the payer’s remaining balance
drops under the floor (OVERLAY_SANDBOX_LOW_BALANCE_CUTOFF_CENTS, default $1) or a tick debit is
declined, the lease flips to stopping, pending commands and queued/running turns are cancelled
with a usage_limited terminal error, and the provider sandbox is deleted by the reaper pass; a
turn’s settle is just the final meter tick plus bootstrap-hold release. All metering and cleanup
reconnects use {resume:false} — passive reads never wake an idle-stopped sandbox. Some providers
only publish cumulative usage counters once a session stops, so while a sandbox runs the
meter bills the elapsed window against a virtual wall-time cursor instead; teardown paths
(reap, revoke) stop the instance, take one final meter read that reconciles real totals
against the virtual cursor, then delete — session usage is never lost. The meter also enforces
the lease’s idle window: Box has no native idle timer, so a running sandbox whose lease shows no activity for
usage.idleTimeoutMs (default 15 minutes) is provider-stopped while the lease stays running —
every turn-slice acquire heartbeats usage.lastActiveAt, so an actively working sandbox never
cools mid-turn, and the next turn’s resume:true acquire wakes it again. A running lease past
reservedUntil (the 24-hour hard cap) is reaped on the next tick regardless of whether its
sandbox still answers. The meter driver runs from
the BFF agent-environments/operations/reconcile route (Convex cron calls it).
The standalone sandbox route keeps the worst-case reservation model
because it always denies outbound network access and can make a byte-exact provider-cost promise;
it rejects oversized artifacts before provider transfer, and its reservation includes the maximum
bounded artifact transfer. The default iad1 region and its
configured unit rates must move together. Blank pricing or buffer variables fall back to
conservative defaults rather than silently becoming zero.
Workspace Stripe ownership is account-keyed, never admin-keyed. Owners and admins may initialize the account and open checkout, top-up, verification, and portal flows, but Stripe metadata and durable subscription state bind to billingAccountId plus workspaceId; userId is retained only as the initiating actor. The Convex Stripe webhook preserves the personal metadata path, deduplicates event IDs, and rejects stale subscription state with providerEventCreatedAt.
Usage reservations have an evidence-gated recovery path. Work that expires before a provider starts is released automatically. Work that may have reached a provider is quarantined as reconcile_required; it must be finalized or released with a durable provider or internal-operation evidence reference. The five-minute Convex cron exposes queue depth and oldest age but never guesses an ambiguous charge.
Finalized usage preserves retail cost and actual provider cost independently. UsageRepository.getBillingAccountOperationalReport reports retail credits, actual cost, realized margin, cost coverage, reconcile queue depth, and the count older than the 15-minute response SLA. The workspace billing UI exposes this only to owners and admins.
Important code:
src/shared/billing/billing-pricing.tssrc/shared/billing/billing-account.tssrc/shared/billing/billing-account-migration.tssrc/server/billing/billing-runtime.tssrc/server/billing/stripe-billing.tssrc/server/billing/BillingCustomerService.tssrc/server/billing/BillingPayerResolver.tssrc/server/billing/billable-feature-coverage.tssrc/server/billing/automation-workflow-billing.tssrc/server/billing/WorkspaceBillingService.tssrc/server/billing/providers/stripe-billing-provider.tsconvex/billing/subscriptions.tsconvex/billing/accounts.tsconvex/billing/accountSubscriptions.tsconvex/billing/spendLimits.tsconvex/platform/usage.tssrc/shared/billing/usage-reconciliation.tssrc/server/usage/UsageRepository.tsscripts/qa/usage-reconciliation-audit.ts
src/server/app-data/contracts/convex-workspace-billing-provider-contract.test.ts.
Storage
Uploaded files and generated assets are stored through server-mediated flows. Cloudflare R2 remains the managed object store, but it is not used for DNS, web hosting, request proxying, or chat streaming. R2 and S3-compatible object access must remain private and owner-scoped; users should only receive short-lived, validated access.Chat Streaming
Interactive chat streams directly from the Vercel-hosted/api/v1/conversations/act route to the browser. Persistent conversations tee a server-side reader so generation, usage accounting, and message persistence continue after a browser disconnect. Reconnects recover from the authoritative persisted conversation state; there is no external stream relay or stream-auth endpoint.
Observability and Metrics
The measurement layer uses PostHog as the unified metrics sink, emittingoverlay.metrics.* events from both server and client. All emission is
fire-and-forget — a metrics failure must never break a customer-facing
request or crash the UI.
Server-side metrics
src/server/observability/metrics.ts— typed emission contract for BFF requests, Convex functions, model token breakdowns, AgentRun lifecycle, workflow events, uploads, and business rollups.src/server/observability/business-rollup.ts— periodic aggregation of cost-per-active-user-hour, cost-per-chat-turn, and cost-per-automation.src/app/api/v1/_utils/bff.ts—handleBffRouteemits aoverlay.metrics.bff_requestevent on every exit path (early returns + final return) with route, method, status, duration, auth type, workspace, payload size, and Retry-After.src/server/conversations/ActContextService.ts—emitTokenBreakdownestimates per-source token counts (history, memory, skills, tools, attachments, system) and emitsoverlay.metrics.model_tokens.
Client-side metrics
src/shared/observability/client-metrics.ts— isomorphic event bus usingwindow.dispatchEvent(CustomEvent). No PostHog import; theObservabilityClientcomponent listens and forwards to PostHog.src/shared/observability/duplicate-tracker.ts— windowed duplicate request detection (1s, 5s, 15s windows) emittingoverlay.metrics.duplicate_request.src/components/providers/ObservabilityClient.tsx— forwards client metrics events to PostHog viaaddMetricsListener.src/shared/chat/chat-list-cache.ts— emits cache hit/stale/miss events.src/features/chat/components/chat/chatTransport.ts— emits chat-open latency, transcript bytes, and message count.src/features/chat/components/chat/useAgentRunLifecycle.ts— emits AgentRun recovery metrics on terminal transitions.src/contexts/AuthContext.tsx— emits session refresh metrics with trigger source (interval, focus, visibility, manual).
Convex function metrics
convex/lib/metrics.ts—withMetricswrapper for mutation handlers, records function name, duration, and error status to thefunctionMetricstable.convex/platform/metrics.ts— cleanup cron (7-day TTL) and aggregation query for export to PostHog.convex/schema.ts—functionMetricstable with indexes by function name and timestamp.convex/platform/rateLimits.ts— instrumented withwithMetricsas the highest-frequency Convex mutation.
Privacy
Metrics events contain only: route names, opaque IDs, sizes, counts, durations, auth type, workspace ID, and categorized metadata. No conversation content, message bodies, authorization tokens, private workspace JSON, or raw user data is sent to analytics.Convex
Development commonly uses a separate dev deployment from production. Follow Worktree Staging QA and Convex Workflow for the deployment lane that matches the current revision. Deploy Convex only from the matching release worktree:convex:push:all, and do not pass .env.local to production Convex deploy commands; that can point deploys at the dev slug.