Guidance for coding agents working in this repository (a pnpm + vite-plus TypeScript monorepo for marimohub). Read this before making changes.
| Purpose | Command | Expected on success |
|---|---|---|
| Install | pnpm install --frozen-lockfile |
exit 0 |
| Check (fmt+lint) | pnpm check |
exit 0, no errors |
| Typecheck | pnpm typecheck |
exit 0, no errors |
| Tests | pnpm test |
all pass |
| Coverage | pnpm test:coverage |
report (v8) |
| Build | pnpm build |
exit 0 |
The toolchain is vite-plus (vp). pnpm check runs vp check
(format + lint; it does not run the TypeScript compiler). pnpm typecheck runs
vp run -r typecheck — each package's tsc --noEmit. CI runs both. pnpm test
runs each package's test script (vitest); pnpm build builds every package.
Use these as the done-criteria for any change.
- Ports and adapters. Every external dependency — storage, compute, identity — sits behind a TypeScript interface (a port). The domain depends on the interface, never on a vendor SDK.
packages/coreholds the domain model, services, and the port interfaces. It imports no vendor SDK — nothing that speaks to a specific provider or performs I/O. Its deps are generic, side-effect-free utilities only:ulidx,zod,better-all,@opentelemetry/apiand@opentelemetry/api-logs(no-op tracing/logs facades unless an entrypoint registers a provider), the format serializerssmol-tomlandyaml(core rendersmarimo.tomland integration config files), andfflate(in-memory archive decompression for workspace ingest). A serializer is a pure function, not a vendor, so a port around it would buy no substitutability. Anything that reaches something — a store, a cluster, an IdP — is a port.- Adapters (
packages/storage-*,packages/compute-*,packages/auth-*,packages/object-browser-*,packages/notify-*,packages/source-control-*, and the credential, secret, and DuckDB runtime packages) implement the ports.packages/apiwires the services to Hono/OpenAPI routes via@hono/zod-openapi. packages/configis the ONLY package that imports concrete adapters: it readsMARIMOHUB_*env vars, selects an adapter per*_BACKENDselector, and wires the system together.- Entrypoints compose everything:
apps/server(Node, for Docker/k8s) andexamples/cloudflare-worker(Cloudflare Workers). Seedevelopment_docs/architecture.md.
Dependencies point inward only. core and api never import an adapter;
adapters depend on core's port interfaces. config (and the entrypoints) are
the only places concrete adapters are imported. Reject PRs that violate this
— for example, an adapter import in packages/core or packages/api. The rule
is enforced mechanically: a
no-restricted-imports override in vite.config.ts (files packages/core/**,
packages/api/**) bans @marimo-hub/{storage,compute,auth,credentials,secrets}-*
imports, @marimo-hub/{notify,object-browser,source-control}-* imports, and
@marimo-hub/duckdb-wasm-runtime. A colocated package-dependencies.test.ts in
each of core and api fails if one of these packages appears in its
package.json.
- Formatting (from
.oxfmtrc.json): tabs for indentation, single quotes, semicolons,printWidth: 100,trailingComma: all. Runpnpm check(orvp fmt) before finishing; CI fails on unformatted files. - Tests are colocated
*.test.tsfiles using vitest, with theMemoryBuckettest double imported from@marimo-hub/core/testing. Reusable conformance suites live at@marimo-hub/core/testing/contract(bucketContract, run by every storage adapter),@marimo-hub/core/testing/compute-contract(computeContract, run by the hermetic compute adapters), and@marimo-hub/core/testing/browse-contract(browseContract, run by every browsable integration kind against a live catalog — env-gated, served in CI by theCatalog conformanceworkflow). Result-envelope assertions (expectExecResult,expectFileResult) are exported from@marimo-hub/core/testing— prefer them over hand-rolled{ success, … }checks. - API response envelope is always
{ success: true, data }or{ success: false, error: { code, message } }(seepackages/api/src). Sole exception: routes serving raw content (e.g. the notebook HTML snapshot atGET …/notebooks/{nid}/html) return the bytes directly on success; their errors still use the envelope. - Committed specs.
packages/api/openapi.yamlandinternal/schemas/{bucket,integrations}.ymlare generated from the zod schemas; drift tests fail the build when they are stale. Regenerate withpnpm schemas:generate. CI diffs each spec againstmainwith oasdiff and fails on breaking changes. Seeinternal/schemas/README.md. - Frontend (
packages/web) is a React 19 SPA using Tailwind v4 (@tailwindcss/vite) with shadcn-style UI (class-variance-authority,clsx,tailwind-merge,lucide-react,sonner) plusreact-aria-components, TanStack Query, and React Router. Helpers live insrc/lib/utils.ts; theming insrc/context/ThemeContext.tsx; UI primitives insrc/components/ui/. It is plain CSS no longer — use Tailwind utilities.
- Final task: remove AI slop comments. Always add a to-do item to the end of your task list to review your changes and strip AI-slop comments — verbose restatements of what the code plainly does, narration of the change ("now we also…"), or obvious-from-signature doc blocks. Keep only comments that explain why (non-obvious intent, invariants, gotchas), matching the surrounding style.
_system/catalog.json (see packages/core/src/paths.ts) is the only mutable
pointer in the catalog snapshot chain. All writes to this object go through
CatalogService.mutateSnapshot
(packages/core/src/services/catalog/CatalogService.ts). This method uses an
ETag compare-and-swap (conditional PUT) with retries.
These CAS-managed records also have one writer each:
IdentityServiceowns each identity at_system/identities/{user-id}.json.CliAuthorizationServiceowns each short-lived CLI login grant at_system/cli-authorizations/{authorization-id}.json.ProjectAlertStoreowns each project alert configuration atprojects/{pid}/alerts.json.SessionServiceowns each editor claim at_system/editors/{pid}/{nid}.json.SessionService.claimApp/releaseAppowns each app claim at_system/apps/{pid}/{nid}.json.SessionServiceowns each monotonic version-prune cutoff at_system/version-prune-cutoffs/{pid}/{nid}.json.NotebookProposalServiceowns each proposal publication atprojects/{pid}/notebooks/{nid}/proposals/{proposal-id}/publication.json.ProjectIntegrationsStoreowns each project integration head atprojects/{pid}/integrations/{iid}/integration.json.OrgIntegrationsStoreowns each org integration head at_system/integrations/{iid}/integration.json.
Each integration store also owns its immutable versions/{n}.json history and
its integrations/_names/{name}.json uniqueness claim. Version writes use
create-if-absent. Name claims use the same pattern as app claims.
Deleting a notebook or project can delete its subordinate claims and objects as
cleanup. Everything else is immutable, append-only, or an operational record,
such as a session, identity, token, or secret. Do not bypass the owners listed
above.
See development_docs/bucket_spec.md.
See plans/ for planned changes and their status.