A URL like /senaite/clients/client-1/W26-0042 does not resolve
through a routing table. Each segment is a real Python object in
the database, and the URL is a walk down the object tree. SENAITE
did not invent this behaviour. It comes from Zope, which has
worked this way since 1998.
The rest of SENAITE is built the same way: proven pieces from long-lived open source projects, with the laboratory domain on top. Knowing the three layers underneath helps when you read the API, plan backups, size a server, or extend the system.
Zope: URLs are paths through objects
Zope is one of the oldest Python web application servers, and it is still maintained. Its defining idea is object publishing. Where Flask or Django map URLs to functions through declared routes, Zope looks up each path segment as a child of the previous object.
Samples live directly inside their client, and analyses inside their sample. Zope walks the path, checks permissions on every object along the way, and hands the last object to a view for rendering.
Three consequences matter for a LIMS:
- The tree is the data model. Which client a sample belongs to is not a foreign key; it is where the sample lives, and what a client’s users may see follows from that location.
- Security is per object. Permissions are evaluated on the object being accessed, and roles can be granted locally on a folder and everything below it. Client access in SENAITE builds on exactly this.
- Components instead of patches. The Zope Component Architecture lets an add-on register adapters, utilities and event subscribers that core code looks up at runtime. A custom ID format, an extra search term or a reaction to a sample being received is a registered component, not a modified core file. That is what keeps customer add-ons upgradeable.
ZODB: an object database
ZODB stores Python objects as they are. There is no ORM, no tables and no schema in the database. A sample with its references and its analyses is written in the shape it has in memory.
What follows from that:
- Every request is a transaction. If a request fails, nothing it changed is written. If two requests change the same object at the same time, one of them gets a conflict and Zope retries it. Partial writes do not happen.
- History is kept until you pack. ZODB appends new versions of changed objects instead of overwriting them, so the database file grows with every change. Packing removes old versions. The audit trail of SENAITE is not affected: the change history of a sample is stored on the sample itself, not in old database versions.
- New fields do not need a database migration. An object that has no value for a newly added field returns the field’s default. Catalog indexes and computed values still need an upgrade step, which is why SENAITE releases ship with them.
The trade-off is the absence of ad hoc queries. There is no SQL
JOIN; everything you want to filter or sort on needs a catalog
index. The catalog post
covers how that works and where it bites.
Plone: content types, workflows, users
Zope on its own is a toolkit. Plone is the content management system built on it, and it supplies the parts that make a LIMS manageable.
- Content types. Every kind of object, from a sample type to a worksheet, is a content type with a schema, forms and validation. SENAITE uses two generations side by side: the older Archetypes framework and its successor Dexterity. Sample types, contacts, worksheets and most setup types are Dexterity types in SENAITE 2.x. Samples, analyses, analysis services and clients are still Archetypes and are being migrated release by release. For users the difference shows mainly in the forms.
- Workflows. Each type has a workflow with named states,
guarded transitions and a permission map per state. A sample
moves through states such as
sample_due,sample_received,to_be_verified,verifiedandpublished, and in each state the workflow decides who may edit which field. Every transition is recorded with actor and time. - Users, groups and roles. Plone’s user management, including connections to LDAP or Active Directory, is what SENAITE roles such as Lab Clerk, Analyst, Verifier or Lab Manager build on. Permissions are granted to roles, not to people, and the checks happen in the framework rather than scattered through application code. An analyst may submit a result but not verify it; whether the person who submitted a result may also verify it is a setup option (Allow self-verification of results), not a code change.
What the stack means in practice
For administrators: a backup is more than one file. ZODB keeps
objects in Data.fs and binary files, such as published report
PDFs, in a separate blob storage directory. Back up both, from the
same point in time, and test the restore. Schedule packing; a
database that is never packed only grows.
For integrators: the object tree explains the JSON API. Every object has a URL that follows the tree and a UID that never changes, and references between objects are UIDs. Use the UID for anything you store on your side; IDs and paths are for humans. The JSON API post shows the requests.
For developers: the pattern for extending SENAITE is a marker interface for a capability, an adapter for behaviour, a Dexterity type for new content, and workflow definitions with guards for new states. Our customer add-ons are built this way, so they survive core upgrades without patching.
For IT and procurement: none of this is new technology. Zope was released as open source in 1998, ZODB and Plone have been developed for more than two decades, and all three are stewarded by open source foundations. For a system that has to run for a decade and pass audits along the way, a well-understood stack is an advantage.
Further reading
The official documentation of the three projects: