RIDING BYTES
← All insights

July 21, 2026 · 6 min read

SENAITE under the hood: Plone, Zope, ZODB, and why it matters

SENAITE is a domain layer on three long-lived open-source technologies. Here is what Zope, ZODB, and Plone each do, and how that shapes the API, backups, and the way you extend the system.

SENAITE under the hood: Plone, Zope, ZODB, and why it matters

A URL like /senaite/clients/acme-labs/samples/H2O-2026-0042 does not resolve through a router. Each segment is a real Python object in the database, and the URL is a walk down the object tree. That behaviour is not something SENAITE built. It comes from Zope, which has been doing it since 1998.

The rest of SENAITE fits together the same way: pieces borrowed from long-lived open-source projects, with a laboratory domain layered on top. Understanding the three technologies underneath changes how you read the API, how you plan backups, and how you extend the system.

Zope: the web server that maps URLs to objects

Zope was one of the earliest Python web frameworks. Twenty-seven years on, it still ships regular releases and still does one thing that no popular modern framework does: publish objects directly through URLs. Where Flask or Django ask you to declare routes, Zope asks you to build a tree of Python objects. The URL is a path through the tree.

Each segment is a real Python object living in the ZODB. Zope walks the path, checks permissions at every step, and hands the final object off to a view for rendering. Add a segment, add a child object.

For SENAITE, that has three practical consequences:

  • Traversal is free. A client with a hundred samples with fifty analyses each is a hundred-by-fifty-deep URL space out of the box. No route table to maintain.
  • Security is per-object. Every object in the tree carries its own access-control list. A user who can view a client’s samples can be prevented from viewing a specific one without a code change.
  • The Zope Component Architecture (ZCA) is how add-ons hook into the core without patching it. When a new SENAITE feature registers a marker interface, an adapter, or a subscriber, this is the machinery it uses. Extending SENAITE is largely a matter of registering the right ZCA component.

ZODB: an object database with no schema

ZODB is Zope’s storage layer, and it removes an entire class of problem from a system like SENAITE. Python objects are stored as they are — no ORM in the middle, no schema migration, no translation to rows and columns. A Sample with references to a Client, a list of Analysis objects, and a Batch is written to disk in the same shape it has in memory.

Three things follow:

  • Persistence is transparent. Anything reachable from a persistent root is stored. There is no save() at the end of a request handler.
  • Every request is a transaction. If a request raises, the transaction rolls back and no partial state survives. Two writes racing produce a ConflictError and the losing side retries. That is why concurrent worksheet edits stay safe.
  • Adding a field is a code change, not a migration. Existing objects gain the new field with its default on next access.

The tradeoff is real. There are no SQL joins. Everything that would be a query in a relational database is a catalog lookup here — a Zope catalog is an indexed view maintained alongside the objects it references. That is why “catalog” and “index” show up so often in SENAITE conversations, and why a badly-indexed field is a much more expensive problem here than in a system where you can bolt a new index on later.

Plone: content types, workflows, roles

Zope by itself is closer to a toolkit than a system. Plone is the opinionated framework built on top of it, and it supplies the pieces that made building a LIMS on this stack tractable in the first place.

  • A content type framework (Dexterity) for defining domain objects with a schema, form generation, and validation. Every AnalysisRequest, Client, Worksheet, and ReportTemplate is a Dexterity type.
  • A workflow engine with named states, guarded transitions, per-state permission maps, and a full transition history on every object. This is what makes “sample received → to be verified → verified → published” a first-class concept rather than a status column.
  • A role and permission model that is granular per-object, per-transition, and per-field. A Sampler can move a sample from scheduled_sampling to sample_received but cannot edit a result on it; the model expresses that directly rather than through scattered permission checks in application code.

Plone brings twenty-plus years of security review, a mature user and group model, and the confidence that comes from a system used in production by governments, publishers, and universities. For a laboratory system where audit and access control are not optional, that inheritance is doing a lot of work.

What the stack means in practice

For integrators, the object tree is why the JSON API URL scheme looks the way it does (/@@API/senaite/v1/analysisrequest/<UID>) and why references between objects are UIDs, not foreign keys. Reading the tree structure is reading the API surface.

For administrators, ZODB is why a full backup is one file (Data.fs) and why database packing is a scheduled maintenance task, not something you forget until the disk fills up. Growth trends and pack schedules deserve real attention.

For developers extending SENAITE, the pattern is: marker interface for a new capability, adapter for behaviour, Dexterity type for new content, XML plus Python guards for new workflow. Everything else composes from there.

None of this is exotic. It is a stack that has been in production for two decades. That is exactly the point — a laboratory system is a bad place to be an early adopter.

Further reading

Official documentation for the pieces:

Bring this to your lab

Whether you are evaluating SENAITE, scaling it across sites, or wiring up instruments and monitoring, the team that wrote the platform is the team you talk to.

Start a conversation