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
ConflictErrorand 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, andReportTemplateis 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_samplingtosample_receivedbut 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: