One rule
If a decision has consequences someone might later be asked to justify, the server makes it. Everything else can live on the desktop, where it will be faster and more pleasant.
This sounds obvious and is violated constantly, usually for good reasons: the client already knows the answer, a round trip is slow, the check is simple. Each shortcut is individually reasonable and collectively they move authority to a process running on a laptop the organisation does not fully control.
Why the client cannot be trusted
Not because architects are adversaries — they are not. Because the client runs in an environment where its behaviour can be altered, its traffic observed and its state edited, and because a client bug is indistinguishable from a client attack from the server's perspective.
A plugin that decides whether the user may publish will one day decide wrongly, and there will be no record of why. A server that decides can be logged, tested, and reasoned about.
Where this goes wrong in practice
Client-side permission checks
Hiding the publish button for read-only users is good UX. It is not authorization. The server must reject the publish regardless, because the button is not the only way to send the request.
Client-decided concurrency
A plugin that checks whether the model changed and skips the publish if it did is making a correctness decision with stale information. The server compares the base revision, because only the server knows the current one.
Database credentials on the desktop
The shortcut that ends worst. If the plugin connects directly to PostgreSQL, every architect's laptop holds credentials to the entire repository, all authorization becomes advisory, and the audit trail records the database user rather than the person.
The plugin should never receive database credentials, and tokens should never be written into model files. Model files get emailed, committed and shared — that is what they are for.
What the desktop should own
Being clear about the other half, because an over-thin client is its own failure:
- The working copy. Editing against the local model is what makes the tool responsive. Round-tripping every change to a server would make modelling unpleasant.
- Crash recovery. If the tool dies mid-edit, the local copy should survive and be offered back. This is the single feature architects notice most.
- Lease renewal. The client keeps its own lease alive while it is running. The server decides whether to grant it.
- Presentation. Which repositories and projects to show, how to lay out the browser — with the caveat that the server must not send what the user may not see.
The recovery case
Worth dwelling on because it is where the boundary gets tested. An architect edits for three hours, the laptop crashes, and the lease expires. Someone else takes the lease and publishes.
The right behaviour: the client preserves the working copy and offers it on restart. The user can see their work, and can attempt to publish it — at which point the server rejects it as stale, because it is. Nothing is lost silently and nothing is overwritten silently.
The wrong behaviour is either extreme: discarding the working copy because the lease expired, or letting the client publish it because it held a lease when it started.
Offline, and what it should mean
Architects work on trains. Some capability without a connection is a reasonable expectation, and the boundary described here constrains what it can be.
What can work offline: reading and editing a working copy already on the machine. What cannot: acquiring a lease, publishing, searching the estate, or anything requiring an authorization decision.
The honest design is therefore an editing experience that degrades gracefully — you can work, and you will publish when you reconnect, at which point the concurrency check applies as usual. What must not happen is a client that queues publishes and applies them on reconnection without re-checking, which reintroduces silent overwrites through a side door.
Versioning the API
The plugin ships separately from the server and the two will be out of step, permanently. Architects update plugins when they remember; servers are upgraded on a maintenance window.
This makes API versioning a first-class concern rather than a nicety. A versioned path, additive changes only within a version, and a clear message when a client is too old to talk to the server. The failure to avoid is the plugin that fails with a parse error because a field it did not expect appeared — which is what happens without a discipline about additive change.
What the server owes the client in return
A boundary drawn only in one direction produces a client that cannot give useful feedback. If the desktop is forbidden from deciding anything, it has to be told why a decision went the way it did, in enough detail to say something better than "the server said no".
Three obligations, and the third is the one that gets skipped:
- A machine-readable reason code alongside the human message. The plugin needs to distinguish "your lease expired" from "someone else holds the lease" from "you never had permission", because the right next action differs in each case and only one of them is worth retrying.
- The current state when it rejects. A stale-revision rejection that does not include the revision the server actually holds forces the client into a second round trip to find out what it should have sent.
- A stable error contract. Reason codes are part of the API surface. Renaming one because it read badly in a log breaks every client that branched on it, and clients on desktops update on their own schedule.
Where the boundary blurs: validation
Validation is the honest exception to the one rule, and it is worth saying so rather than pretending the rule is absolute.
Running validation only on the server is correct and produces a miserable experience: the architect works for an hour, publishes, and gets back a list of forty problems introduced forty minutes ago. Running it only on the client is fast and unsound, because a client can be old, patched or simply skipped.
The resolution is to run the same rules in both places with different jobs. On the desktop, validation is a hint: immediate, incomplete, advisory, and free to be wrong in the permissive direction. On the server it is a gate: complete, authoritative, and the only one whose answer counts. The rules should come from one definition so the two cannot drift, which in practice means the server publishes the rule set and the client fetches it rather than shipping its own copy.
What must never happen is the server trusting a client's assertion that validation passed. That is the one rule again, and validation is where teams most often talk themselves out of it for performance reasons.
Upgrades, and the version skew you will actually have
A desktop plugin is installed by people, on machines you do not control, sometimes through a software centre with a monthly approval cycle. Assume at any moment that three plugin versions are in use and one of them is nine months old.
| Situation | What the server should do |
|---|---|
| Client older than the minimum supported version | Refuse, with a reason code that names the required version and where to get it. Silently accepting an old client is how corrupt data enters. |
| Client older than current, newer than minimum | Serve it. Any feature it does not know about simply is not used. This is the normal case and it should be uneventful. |
| Client newer than the server | Serve it, and let the client degrade. Happens whenever a keen architect updates ahead of a server maintenance window. |
| Unknown client | Refuse. An unidentified client is either a bug or someone scripting against the API, and both deserve a look. |
The corollary is that the minimum supported version has to move slowly and be announced, because raising it stops people working with no warning on a morning they had other plans.
Testing across the boundary
The bugs that hurt live exactly on the seam, and neither side's unit tests find them. Three test shapes catch most of it.
- Contract tests that assert the shape of every response including the error responses, run against both sides. Most teams write them for the success paths and discover the gap when an error field is renamed.
- A hostile client. A test client that sends stale revisions, expired lease tokens, permissions it does not have and malformed payloads. If any of those succeeds, the boundary is decorative.
- An old client. Keep the previous release's plugin and run the suite against it before each server release. This is the one people skip, and version skew is the failure it would have caught.
Why this boundary is harder for a modelling tool
Most client-server guidance assumes a thin client rendering data it does not own. A modelling plugin is not that. It lives inside a desktop application that already has its own model in memory, its own undo stack, its own notion of what is open and what is dirty, and no concept of a server at all. The plugin is a guest in someone else's state machine.
That produces a specific class of problem the usual advice does not cover. The host application will happily let a user edit a model the server has since revised, because from the host's point of view nothing has changed — the file on disk is the file it loaded. It will let them undo past the point where a publish happened. It will let them close without saving, discarding work the server was told to expect.
None of that is the host application misbehaving. It is doing exactly what a desktop modelling tool should do, and the plugin has to reconcile it with a server that believes it is the authority. The reconciliation is what makes this boundary interesting, and it is why copying a web application's client-server split does not work here.
The practical consequence is that the plugin needs its own record of what state it last agreed with the server, held separately from the host's model, and it needs to compare the two at every interaction rather than trusting either. That is more bookkeeping than a thin client needs and there is no way around it.
What happens when the server is unreachable
Laptops go on trains. VPNs drop mid-edit. A server maintenance window overruns. The question is not whether the plugin will find itself offline but what it does when it happens, and the answer has to be decided rather than emerging from whatever the error handling happens to do.
The behaviour that works is to fail loudly and immediately on any operation that needs the server, while leaving local editing untouched. An architect who loses connectivity should be able to keep working on what is already open, and should be told clearly that nothing they do will be saved centrally until the connection returns. What they must not get is a plugin that appears to work and quietly queues everything, because a queue that fails to drain three hours later is worse than an error three hours earlier.
The lease is the awkward part. A time-limited edit lease acquired before the connection dropped will expire while the architect is still editing, and the model they hold is now no longer theirs to publish. There is no clever fix. The honest design tells them the lease has expired, offers to save their work to a local file, and makes them re-acquire against current state when they reconnect. Anything that pretends otherwise is deferring a conflict rather than avoiding one.
Keeping the plugin small
Every feature added to the plugin is a feature that has to be shipped to laptops, tested against three host versions, and supported when it breaks on the one machine with an unusual locale. Server features ship once. This asymmetry should be the tiebreaker on every design decision where a capability could live on either side.
It argues for pushing things to the server that feel natural on the client: search, report generation, comparison between revisions, validation rule evaluation, and any rendering that produces something shareable. The plugin's job shrinks towards a small set of verbs — open, lock, publish, release, refresh — plus the reconciliation bookkeeping described above.
The counter-argument is latency, and it is real for anything that runs while the architect waits. The test worth applying is whether the operation happens during a thought or between thoughts. Search as you type is during; generating a comparison report is between, and half a second of server round trip costs nothing there.
Reviewing the boundary in one sitting
The rule is one sentence; keeping it true across releases is a review habit. Here is the sitting that does it, run whenever the plugin or the API changes shape, in the time it takes to read a pull request properly.
Walk every new or changed endpoint and ask the only question that matters: if a hostile client called this with arbitrary arguments, what could it achieve? Any answer that includes "write something the caller's grants do not cover" or "read something scoped away from them" is a finding, regardless of whether the shipped plugin would ever make that call — the shipped plugin is one client of the API, not the definition of it. Then walk the plugin's changes in the opposite direction: has anything moved onto the desktop that decides rather than displays? Grant checks evaluated locally "for responsiveness", validation whose failure is treated as authorization, identifiers trusted from client state — each is the boundary eroding one convenience at a time, and each reads, in isolation, like a reasonable performance optimisation.
Close with the two questions that catch what code review misses. What does the client now persist between sessions, and would a stolen laptop's disk image embarrass anyone? And what does the server log about decisions it made — enough that a disputed action can be reconstructed from the server's records alone, without asking the client's version of events? The boundary between desktop and server is ultimately a boundary between what can be audited and what must be trusted, and the review's whole purpose is to keep everything that matters on the auditable side. Twenty minutes per release, a short written note of what was checked, and the one rule stays a property of the system rather than a memory of its first design.