Skip to main content

Clepit

At a glance

Category
Developer platform
Website
clepit.com
Console
app.clepit.com
Documentation
clepit.com/en/docs
GraphQL API
api.clepit.com/graphql
Realtime
ws.clepit.com
MCP surface
api.clepit.com/mcp
Published pages
clepit.space

Most rich text ends up in a database as a block of HTML. That is fine until you want to do something with it other than show it back: find every page that mentions a customer, render the same document into a web page and an email and a mobile app, or let two people edit it at once without one of them losing a paragraph. By then the words and their formatting are tangled together, and the only thing that can reliably read the document is the editor that wrote it.

Clepit keeps the two apart. A page is a list of blocks, each one a small typed object, and the document is JSON: no markup to parse, and no editor needed in order to read it. That is also why the editor stands alone. @clepit/core is published on npm under an MIT licence and knows nothing about the hosted side, while the workspace at app.clepit.com is what those same documents become once they are given collaborators, permissions, history and an address on the public web.

A page is a list of blocks

A block is four fields: an id, a type, the data that type defines, and any tunes applied to it. A paragraph’s data has a different shape from a table’s, and the type is what tells you which shape to expect, so a stored document can be checked rather than merely parsed. A whole page is a timestamp, a version and the blocks in order, which is small enough to read with your own eyes and to review in a pull request. Nothing in it describes how the page should look: that belongs to whatever is drawing it, which is what allows one document to become a web page, an email and a mobile screen without three copies of it existing.

Drawing a document without a browser

The renderer never writes to the browser directly. It draws through a thin layer with two backings behind it, one that builds real page nodes and one that builds a string, and the same block code runs on both. That is how a published page is drawn on a server where no browser exists, and why what the server produces is the same document the editor would have shown instead of a second implementation left to drift out of step. The string backing takes a sanitiser as a required argument and deliberately has no default: the package’s usual sanitiser builds a page in order to parse, so it cannot run there, and quietly falling back to escaping would strip the inline formatting out of every document without saying so. The gap is left visible where it is used rather than hidden in a default.

Reading costs a reader no JavaScript

The React adapter ships its two halves separately, because they want opposite things. The content component runs on the server and emits finished markup while the page is being built, so a reader receives the document in the first response. The editor component is client-only, since it owns the editor’s lifecycle and there is nothing to own until a browser exists. Reading a Clepit document therefore needs no JavaScript at all. Writing one is where the runtime arrives.

What a page can hold

Twenty-six kinds of block ship in the package. Most are the ones any editor needs: headings, paragraphs, lists, checklists, quotes, code, tables, images, audio, video, files, callouts and dividers. The rest exist because documentation asks for things a writing tool usually ignores. A table of contents that builds itself from the document’s headings and links to each one. Collapsible sections and columns. A card that stands in for another page. A freehand sketch. And an activity block that stores which document to watch rather than a copy of its activity, so it keeps showing what is happening now instead of freezing on the day it was inserted. Formatting inside a block covers the ordinary marks, bold, italic, underline, strikethrough, inline code, highlight and links, along with tooltips, status chips and mentions. The package itself has no runtime dependencies of any kind.

Formulas and diagrams, drawn in the package

Two of those blocks render LaTeX and Mermaid, and both do the whole job in the package: parse the source, work out the layout, draw the result. There is no rendering library underneath and no call to a service to turn a formula or a flowchart into a picture. That is less a preference about dependencies than a consequence of the string backing. A block that reached for something only a browser provides, or for the network, could not be drawn on the server that serves published pages, and a page would then look different depending on who asked for it.

An API reference that lives in the page

Give the OpenAPI block a specification, pasted in or referenced by address, and it renders what the specification describes: the operations, their paths and parameters, the request and response schemas, and the authentication scheme. For each operation it also builds a request snippet in cURL, TypeScript, Dart and Python, generated from the specification rather than typed out by an author who will not remember to update it. The embed block takes the same view of the outside world: it recognises a handful of services it can genuinely display, treats a plain link as a proper outcome rather than a failure for everything else, and refuses outright to render an address it does not trust.

Two people in the same paragraph

A page being edited live is held by a single task on the server, one per page, and every update goes through it in order. That is what makes concurrent editing safe to reason about: there is no second writer racing the first. The document itself is a CRDT, so two people typing in the same paragraph merge rather than overwrite one another, and a client that has fallen behind catches up by exchanging whatever each side is missing. Every update is appended to a write-ahead log before it is broadcast to anyone, so what the other people in the document see has already been durably recorded rather than merely relayed. Permission is enforced on the server and not in the interface: a peer that joins without the right to edit is downgraded to reading, and the session re-checks that right periodically while the document is open, so access taken away lands on someone already typing instead of waiting for them to reload the page.

Every write goes through one door

A document can be changed by someone typing in it and by a program calling the API, and those two paths used to be able to write to the same page independently. They no longer can. An API write is forwarded to the same session that holds the live document, where it is applied as one transaction alongside the live edits, so there is a single order of events rather than two writers with separate opinions about what the page says. Block ids are preserved when the document is written back, because comments are anchored to them and a reconcile that minted new ids would leave every comment pointing at nothing. And when the resulting set of blocks is identical to what is already stored, nothing is written at all.

The one transport a machine key cannot reach

A personal API key works against REST, GraphQL, the GraphQL subscription socket and MCP. It does not work against the collaboration socket, and that is deliberate. Every collaborative update is stamped with the person who made it, and those stamps become the authorship recorded in the page’s history. A machine principal editing there would write an author no person wrote, and undoing that later means rewriting history rather than deleting a row. The line is not between websockets and HTTP: it is whether the transport writes authored history. The rule is enforced by the shape of the code rather than by memory, since accepting a key requires deliberately switching to a different authentication call, and a test fails when a transport does.

A workspace on its own address

Every workspace is a tenant with its own subdomain from the moment it is created, and the tenant is resolved from the address the request arrived on. Which tenant you are in is therefore settled before any of your data is read, rather than by a filter applied afterwards that somebody might forget. Beneath that, the database enforces the boundary itself through row-level security: each request checks out a connection, stamps the caller’s identity onto it, and the pool wipes that state when the connection goes back, so one request’s identity cannot leak into the next request’s queries.

Claiming a domain is not the same as proving it

A workspace on an Enterprise plan can serve its pages from a domain of its own. Claiming one and serving from one are deliberately separate steps: the domain is stored unverified, and the resolver ignores it entirely until a verification record appears in DNS. Anyone can type another company’s address into a form. Only the person who controls that domain can publish the record that makes it live.

Every version the page has had

Clepit keeps revisions rather than a single current state. A snapshot is taken automatically while people work, throttled so that ordinary typing does not produce hundreds of them: a new one is written once ten minutes have passed or ten blocks have changed, whichever comes first. Restoring is a single transaction: the old snapshot is applied, the blocks are reconciled, and the restore itself is written as a new revision, so going back is recorded rather than silently undone. The reconcile preserves the source block ids on purpose, because comments are anchored to blocks, and restoring a page with fresh ids would leave every comment on it without an anchor.

Finding it again

Search runs over a projection of the pages, and the query goes through Postgres’s own web-search parser rather than being assembled into SQL by hand, so a person can type quotation marks and minus signs and none of it is an injection surface. What matters in a shared workspace, though, is where the permission check sits. The search joins the pages table, and that table’s row-level security applies to the join, so the results are already restricted to the pages the person asking may see. Filters (a subtree of the page tree, who last revised it, when it was last edited) compose onto that as additional conditions. Every one of them narrows; none of them can widen, because they all sit behind the same check.

Publishing freezes, sharing does not

These are two different things and Clepit treats them differently on purpose. Publishing a page freezes the current document as a revision, points the page at it, and makes it public: what a visitor reads on clepit.space, at an address like acme.clepit.space/handbook, is that frozen revision and not the edits made since. Unpublishing clears those pointers but keeps the public address, so publishing again later returns to the same URL rather than breaking every link that pointed at it. A share link is the opposite: it serves the live document, so what the recipient sees changes as the page does.

A share link is a token you can revoke, and it can be given an expiry when it is minted. Only a hash of the token is stored, so the link is shown once at creation and cannot be recovered from the database afterwards, by us or by anyone who reaches it. These links grant reading, not commenting: a comment needs an author, and a link holder is not one.

Signing in from your own directory

A workspace can hand authentication to its own identity provider: OpenID Connect on Business, SAML on Enterprise, with SCIM alongside it for directory synchronisation. SCIM covers the user resource that Okta and Entra actually drive, and it makes one deliberate departure from the obvious reading of the standard: a delete deactivates the member rather than erasing them. The specification permits that, and the alternative is a directory sync that can destroy a workspace’s content by removing somebody from a group.

Four ways to talk to it

REST covers forty-four operations under /v1, described by an OpenAPI document generated from the routes themselves rather than written beside them, and continuous integration compares that output against the committed copy, so a route change that skips the registry cannot land quietly. GraphQL covers the application model and carries subscriptions over its own socket. Realtime is a separate process, which is why restarting the gateway does not take the request surface down with it, and it authorises per socket rather than per room: for each event, every connection in the tenant is evaluated in one batched check and only those allowed to see it receive it. MCP exposes the same operations to AI agents as tools, and no tool trusts an id from the caller to decide which workspace it is acting in; writes go through the same services the interface uses, so the permission checks and the audit trail are the same ones.

Who it is for

Teams who need an editor they control, and developers embedding structured content in their own product.

Visit Clepit: clepit.com