@cytario/web

中文文档
9.0.1发布于 2026/11/11AGPL-3.0ES Module构建来源已验证近 30 天下载 6335

Cytario Web — open-source digital pathology viewer and data platform for whole-slide imaging (WSI): OME-TIFF, OME-Zarr, Parquet and GeoTIFF on S3-compatible storage.

原始使用文档
文档目录18 节
  1. Architecture
  2. Plugin model
  3. Using @cytario/web as a package
  4. CLI
  5. Reference Dockerfile
  6. License
  7. Local Development
  8. Prerequisites
  9. Getting Started
  10. Keycloak Organizations
  11. Session Cache (Redis/Valkey)
  12. Database
  13. Testing
  14. Design System
  15. Debugging
  16. Commits
  17. Deployment
  18. Acknowledgements

Cytario Web

CI npm Test Coverage Badge License: AGPL-3.0 GitHub release Node.js GitHub Stars

A web-based viewer and data platform for digital pathology and spatial biology. Cytario Web lets you explore, visualize, and annotate whole-slide imaging data (WSI) and other large-scale scientific datasets (OME-TIFF, OME-Zarr, GeoTIFF, Parquet) stored in S3-compatible object storage — from raw slide browse to quantitative analysis results.

For the hosted product, see cytario.com.

Screenshot

Architecture

LayerTechnology
FrameworkReact Router v8 (SSR), React 19, Vite 7
LanguageTypeScript (strict mode)
Visualizationdeck.gl, Viv, DuckDB-WASM, Apache Arrow
StylingTailwind CSS v4, @cytario/design
AuthOAuth 2.0 via Keycloak, STS for S3 credentials
DatabasePostgreSQL (Prisma ORM), Redis/Valkey (sessions)
CloudAWS SDK v3 (S3, STS); RustFS and other S3-compatible endpoints
CI/CDGitHub Actions, semantic-release, GHCR

Plugin model

Cytario Web supports third-party plugins that contribute additional image formats to the viewer. A plugin is a regular npm package whose default export satisfies the @cytario/plugin-api contract (see packages/plugin-api/src/):

import type { CytarioPlugin } from "@cytario/plugin-api";

const plugin: CytarioPlugin = {
  name: "@vendor/my-loader",
  apiVersion: "^1.0.0",
  register(ctx) {
    // `extension` accepts a string, a string[] of aliases, or a RegExp
    // tested against the URL. See `FormatExtension` in @cytario/plugin-api.
    ctx.formats.register(["myext", "myext.gz"], {
      load: async (url, opts) => {
        const res = await opts.signedFetch(url, { signal: opts.signal });
        // …parse and return { data: Loader, metadata: Image }
      },
      fileTypeMeta: { label: "My Format", icon: "Microscope" },
    });
  },
};

export default plugin;

Loading. The set of plugins is fixed at build time. The CYTARIO_PLUGINS env var (comma-separated npm package names) drives a Vite codegen step that writes app/plugins.generated.ts; the host imports that module at startup, calls plugin.register(ctx) for each entry, and hands the plugin a PluginContext containing a scoped FormatRegistry and a Logger. A plugin can only register handlers under its own name — the registry rejects cross-plugin extension collisions.

Compatibility gate. Each plugin declares an apiVersion semver range, checked against the host's bundled @cytario/plugin-api version on bootstrap. Mismatched plugins are logged and skipped — the host keeps running.

Security boundary. All S3 traffic flows through the host-supplied signedFetch. Plugin-supplied headers pass through sanitizeHeaders (allowlist: Range, If-None-Match, Accept, Cache-Control; always denied: Authorization, Host, Cookie, x-amz-*) before being merged behind the signed headers, so a plugin cannot override the SigV4 signature or smuggle credentials.

Reference. Built-in OME-TIFF and OME-Zarr handlers in app/components/.client/ImageViewer/state/formats/builtins.ts are implemented against the same contract. A minimal stub plugin lives in __tests__/fixtures/noop-plugin/ and exercises the registry, the apiVersion gate, and the FILE_TYPE_REGISTRY auto-derivation.

Using @cytario/web as a package

@cytario/web can be installed as an npm dependency and assembled into a deployable container together with one or more format-handler plugins. This is how Cytario Enterprise Edition is built: the AGPL-licensed open core (@cytario/web) is bundled with proprietary plugins (for example, vendor-specific microscopy format loaders) to produce a single deployable image. Anyone with a plugin that satisfies the @cytario/plugin-api contract can follow the same pattern.

License obligation. @cytario/web is licensed under AGPL-3.0. Distributing or operating an assembly that includes it — including over a network as a service — triggers the AGPL's source-disclosure requirement: the complete corresponding source of the assembly (including any proprietary plugins linked into it) must be made available to its users under AGPL-3.0. If that is incompatible with your distribution model, a commercial license is available — contact us at cytario.com.

The consumer's job is to install the packages, set CYTARIO_PLUGINS, and invoke the bundled CLI:

# .npmrc — only required if any of the plugins ship from a registry
# other than public npm. The example below routes a hypothetical
# closed plugin to GitHub Packages while keeping @cytario/web and
# @cytario/plugin-api on public npm.
@your-org/closed-plugin:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GH_TOKEN}
npm install @cytario/web @cytario/plugin-api @your-org/my-plugin

# Production build with the bundled plugin set.
CYTARIO_PLUGINS=@your-org/my-plugin npx cytario-web build

# Dev server with HMR.
CYTARIO_PLUGINS=@your-org/my-plugin npx cytario-web dev

# Production server against the existing build/ output.
npx cytario-web start

CYTARIO_PLUGINS is a comma-separated list of npm package names. Each package's default export must satisfy the @cytario/plugin-api contract.

CLI

CommandBehaviour
cytario-web buildCodegen (Vite plugin reads CYTARIO_PLUGINS) + react-router build against the installed package root.
cytario-web devCodegen + react-router dev. Extra args (--port, --host, …) are forwarded.
cytario-web startNODE_ENV=production node server.ts against the bundled build/server/index.js.

All subcommands operate against @cytario/web's own install directory; the consumer never needs to know the on-disk layout.

Reference Dockerfile

# syntax=docker/dockerfile:1.7
FROM node:24-slim AS build
WORKDIR /app
COPY .npmrc package.json package-lock.json ./
# BuildKit secret: GH_TOKEN is exposed as an env var only for this
# RUN. It is never written to a layer and never appears in `docker
# history` or `docker inspect`. Do NOT add `ARG GH_TOKEN` — that
# would bake the value into image metadata.
RUN --mount=type=secret,id=gh_token,env=GH_TOKEN \
    npm ci
ENV CYTARIO_PLUGINS=@your-org/my-plugin
RUN npx cytario-web build

FROM node:24-slim
WORKDIR /app
COPY --from=build /app .
ENV NODE_ENV=production
EXPOSE 3000
CMD ["npx", "cytario-web", "start"]

Build the image with the BuildKit secret bound from your environment (or a file):

# Docker — environment source (e.g. CI runner with a GH_TOKEN secret):
GH_TOKEN=… docker build --secret id=gh_token,env=GH_TOKEN -t my-cytario-image .

# Docker — file source:
docker build --secret id=gh_token,src=./gh_token.txt -t my-cytario-image .

# Podman — only file source is supported (no env= flavor); write the
# token to a temp file first:
printf '%s' "$GH_TOKEN" > /tmp/gh_token && \
    podman build --secret id=gh_token,src=/tmp/gh_token -t my-cytario-image . && \
    rm -f /tmp/gh_token

The # syntax=docker/dockerfile:1.7 parser directive at the top of the Dockerfile is honored by Docker's BuildKit and silently ignored by Podman's native parser — the rest of the file (multi-stage, --mount=type=cache, --mount=type=secret) works on both engines.

Mixed-registry note. @cytario/web and @cytario/plugin-api publish to public npm. Plugins are free to publish wherever they like — public npm, GitHub Packages, or a private registry. Because a single scope-wide @cytario:registry directive cannot route both public-npm and GitHub-Packages packages under the same scope, consumers pin individual plugin packages with a per-package <pkg>:registry=… override as shown above.

License

This project is licensed under AGPL-3.0. The source code is publicly available to provide full transparency and ensure long-term access for our users, independent of Cytario as a company.


Local Development

Prerequisites

The application requires several backend services. A local cluster is provided via Podman:

cd devenv
podman kube play local-deployment.yaml
ServicePortDescription
Keycloak8080Identity provider (admin/admin)
RustFS9000, 9001S3-compatible object storage (console on 9001)
PostgreSQL5433Application database
Valkey6379Session cache (Redis-compatible)

To stop: podman kube down devenv/local-deployment.yaml

Postgres 18 data layout: the postgres:18 image stores data in a major-version subdirectory and mounts at /var/lib/postgresql (not /var/lib/postgresql/data — see docker-library/postgres#37). If you previously ran this cluster with the older 16/17-based file, your devenv/*-data Postgres dirs hold the old flat layout and the 18 container will refuse to start over them. The devenv data dirs are disposable — rm -rf devenv/keycloak-db-data devenv/cytario-db-data and replay the cluster (then re-run the OpenTofu bootstrap). Postgres 17 and 16 also mount fine under the parent path, so nothing is version-locked.

Keycloak

The cluster runs a stock Keycloak with the organization feature enabled. First boot gives you an empty master realm — provision it either with the one-shot OpenTofu bootstrap below (recommended) or by hand through the admin console (http://localhost:8080, admin/admin):

  1. Create a realm (e.g. cytario) and toggle Organizations on (Realm settings → General).
  2. Create a confidential client cytario-web with a secret, valid redirect URIs for http://localhost:3000/*, and web origins http://localhost:3000. Point BASE_URL/CLIENT_ID/CLIENT_SECRET in .env at it.
  3. Create a service-account client for the Keycloak Admin API (see KC_ADMIN_CLIENT_ID in .env.template) with manage-realm, manage-users, view-realm from the realm-management client.
  4. Create a user, an organization, assign the user to it, and add them to groups as needed (see Keycloak Organizations).

devenv/terraform/ creates everything the app needs in one apply: the realm with Organizations enabled, the cytario-web client (secret 1234567, matching .env.template) with the built-in organization client scope (the nested org claim the app reads) and a top-level groups claim, a demo organization, the Lab/TeamX org groups plus the admins group that grants the * admin scope, and two users ([email protected] / demo-admin, [email protected] / demo-viewer):

cd devenv/terraform
tofu init
tofu apply          # ~10s against the running Keycloak

# Org-group membership goes through the org-scoped admin API, which the
# terraform provider cannot express — the companion script does it:
./org-membership.sh cytario "$(tofu output -raw organization_id)"   [email protected]:admins [email protected]:Lab   [email protected]:Lab

Then set in .env:

BASE_URL=http://localhost:8080/realms/cytario
CLIENT_ID=cytario-web
CLIENT_SECRET=1234567

tofu destroy removes the realm; re-running apply after a podman kube down (data dirs kept) converges the state.

Why stock Keycloak? Cytario's production identity stack carries custom protocol mappers (for the RustFS groups claim) in a private image. For the open-source local setup, a stock Keycloak covers authentication and tenant routing; the storage mappers are only needed when connecting a RustFS provider for bucket-policy sharing (see below).

RustFS (S3-compatible storage)

The cluster runs RustFS — the S3-compatible object storage the platform targets for self-hosted deployments. It boots with web-identity federation against the local Keycloak realm (client cytario-web, claim groups).

  • Console — http://localhost:9001 (rustfsadmin/rustfsadmin). Create buckets, IAM policies, and groups here.
  • Connecting from the app — a RustFS provider connection needs an endpoint (http://localhost:9000 for the local cluster) and the operator-side provisioning described in the user guide's RustFS storage onboarding runbook: per-organization IAM policies and the claim-named groups they attach to. In an OSS deployment (no admin portal), the connection selectors read from a deploy-time catalog file — see devenv/providers.example.yaml for both an AWS and a RustFS entry.

Object storage in .env

The browser talks to object storage directly (SigV4-signed fetches). The app's Content-Security-Policy connect-src is derived per request from the organization's provider catalog, so every storage endpoint the organization has a provider connection for — including the local RustFS — is admitted with no allowlist configuration. Endpoints must use https: in production; local dev environments can admit a plain-http object store via CYTARIO_DEV_CONNECT_HOSTS:

# .env — local dev only: admit the local RustFS object store
CYTARIO_DEV_CONNECT_HOSTS=http://localhost:9000

Getting Started

npm install
cp .env.template .env    # Pre-configured for the Podman cluster
npm run dev

Keycloak Organizations

The app uses Keycloak Organizations as the tenant boundary. Every session must carry an active organization — sessions without one are redirected to /onboarding and cannot reach any tenant-scoped route.

The local Podman cluster boots a stock Keycloak with KC_FEATURES=organization enabled but an empty realm — follow the setup steps under Keycloak above to create the realm, client, and organization. To run against a custom Keycloak deployment:

  1. Enable the Organizations realm feature (KC_FEATURES=organization on the Keycloak server and the per-realm toggle in the admin UI).
  2. Grant the cytario-web-admin service account view-realm + manage-realm on the realm-management client. No dedicated view-organizations role exists — the broader realm roles are required.
  3. Assign every login-eligible user to at least one organization. Users without an organization land on /onboarding.

Group membership inside an organization (subgroups under /admin/users) drives in-tenant authorization. A /admins subgroup at a given scope confers admin rights over that scope; the * sentinel scope covers the entire organization.

Session Cache (Redis/Valkey)

Sessions hold OAuth access/refresh/ID tokens and short-lived STS credentials. TLS is required in production. The app refuses to boot when NODE_ENV !== "development" unless one of the following is true:

Env varValueMeaning
REDIS_TLS"true"Wrap the ioredis connection in TLS (recommended).
REDIS_CA_CERTPEMOptional CA bundle for self-signed deployments. Multi-line PEM string.
REDIS_TLS_SERVER_NAMEhostnameOptional SNI / certificate hostname override.
REDIS_INSECURE_ALLOW_PLAINTEXT"true"Explicit opt-out for trusted private networks. Logs a warning. Not for use on shared infrastructure.

The local Podman cluster runs Valkey without TLS, which is allowed because NODE_ENV=development. Managed Valkey deployments (helm chart, AWS ElastiCache, etc.) should set REDIS_TLS=true. Valkey reuses the standard 6379 port for TLS when tls.enabled is set — it does not move the listener to 6380 and refuses plaintext on the same port — so leave REDIS_PORT at 6379 unless your provider explicitly publishes a separate TLS endpoint.

In a production cluster, a common pattern is a cluster-internal CA (e.g. cert-manager + trust-manager distributing the CA's public cert as a ConfigMap) whose PEM is then fed to the app through the normal REDIS_CA_CERT env var path — the app has no knowledge of the trust source. The CA's public cert is distributed to every namespace as a cytario-internal-ca ConfigMap by trust-manager, and the cytario-web helm chart's redis.caCertConfigMap.{name,key} wires it into the pod as REDIS_CA_CERT via valueFrom.configMapKeyRef. The app sees the PEM through the normal env var path — no code-side knowledge of the trust source is required.

Database

PostgreSQL with Prisma ORM. Connection configured via DATABASE_URL in .env.

npx prisma migrate dev --name <migration-name>   # Create + apply migration
npx prisma migrate deploy                         # Apply pending migrations
npx prisma studio                                 # Database GUI
npx prisma generate                               # Regenerate client

Cloning the DEV database

scripts/clone-dev-db.mjs copies the DEV PostgreSQL (schema + data) into the local Podman cluster's Postgres. It uses the pg driver rather than pg_dump/psql, so it works through pgbouncer (transaction-pooling mode) — pg_dump fails on session-level operations there.

# 1. Make sure the local cluster is up and Postgres is reachable on :5433.
cd devenv && podman kube play local-deployment.yaml && cd ..

# 2. Clone. Pass the DEV DATABASE_URL as the source and the local URL
#    (from .env.template) as the target. The script refuses to write to
#    any host that doesn't resolve to loopback.
npm run clone-dev-db -- \
  "$DEV_DATABASE_URL" \
  "postgresql://cytario:cytario@localhost:5433/cytario"

# 3. Apply any migrations the local checkout has that DEV doesn't yet.
DATABASE_URL="postgresql://cytario:cytario@localhost:5433/cytario" \
  npx prisma migrate deploy

The clone copies every public-schema table, including _prisma_migrations, so Prisma recognises which migrations are already applied. If the target database already has tables, the script lists them and asks for confirmation before dropping them.

Testing

npm test              # Unit & component tests (vitest, watch mode)
npm run coverage      # Unit tests with coverage report

E2E tests (Playwright) live in a sibling repository and are triggered automatically on every PR via cross-repo dispatch.

Design System

To develop @cytario/design — or any @cytario package — locally, point CYTARIO_LOCAL_PATHS at the sibling checkout before starting the dev server:

CYTARIO_LOCAL_PATHS='{"@cytario/design":"../cytario-design"}' npm run dev

Each entry is aliased to the sibling's src/index.ts, so Vite serves its TypeScript source directly and picks up edits via HMR — no npm link, no rebuild step. Subpath imports follow the sibling tree (styles/tokens.css → src/…); the compiled stylesheet (styles.css → dist/index.css) is served from the sibling's dist/, so run npm run watch:css in the design repo alongside.

With the variable unset, the app uses the published packages from npm — nothing about the build changes.

Debugging

The app uses Zustand for state management with the devtools middleware. Install the Redux DevTools browser extension to inspect store state and actions.

Commits

Conventional Commits enforced via commitlint (feat, fix, docs, refactor, test, build, ci, chore, etc.).

Deployment

npm run build    # Outputs build/server and build/client
npm start        # Production server

For containerized deployments, see the Dockerfile. The image is built on distroless (gcr.io/distroless/nodejs24-debian12); database migrations run automatically on startup via docker-entrypoint.mjs.

Acknowledgements

Built on Viv, a library for multiscale visualization of high-resolution, highly multiplexed bioimaging data on the web, developed by the HIDIVE Lab at Harvard Medical School.

版本与发布记录

@cytario/web 共发布 219 个版本,首次发布于 2026/05/16;过去 12 个月发布了 219 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
9.0.1latest2026/11/11今天
9.0.02026/11/101 天前
8.16.112026/11/037 天前
8.16.102026/10/3011 天前
8.16.92026/10/3012 天前
8.16.82026/10/2913 天前

关于 @cytario/web 的常见问题

@cytario/web 是什么?

Cytario Web — open-source digital pathology viewer and data platform for whole-slide imaging (WSI): OME-TIFF, OME-Zarr, Parquet and GeoTIFF on S3-compatible storage.

如何安装 @cytario/web?

在项目目录运行 npm install @cytario/web,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 cytario-web,可以用 npx cytario-web 直接运行。

@cytario/web 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/cytario__web 是否存在。

@cytario/web 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@cytario/web 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24.10.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@cytario/web 可以商用吗?

@cytario/web 使用 AGPL-3.0 许可。最严格的 Copyleft:除分发外,通过网络向用户提供基于它的服务也需要开源对应源码。闭源 SaaS 使用前务必评估。以上为概括,不构成法律意见,请以许可原文为准。

@cytario/web 的下载量有多少?

根据 npm 官方下载统计,@cytario/web 在 2026/10/11 – 2026/11/09 这 30 天内被下载 6,335 次,日均约 211 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cytario/web 还在维护吗?

最新版本 9.0.1 发布于 2026/11/11(今天);至今共发布 219 个版本,其中过去 12 个月发布了 219 个;首次发布于 2026/05/16。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。